Skip to content

Commit 95aeecc

Browse files
authored
Add InMemoryMetricReader to metrics SDK (#2540)
1 parent 97a36ea commit 95aeecc

File tree

3 files changed

+114
-2
lines changed

3 files changed

+114
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
([#2528](https://github.com/open-telemetry/opentelemetry-python/pull/2528))
1717
- Fix delta histogram sum not being reset on collection
1818
([#2533](https://github.com/open-telemetry/opentelemetry-python/pull/2533))
19+
- Add InMemoryMetricReader to metrics SDK
20+
([#2540](https://github.com/open-telemetry/opentelemetry-python/pull/2540))
1921

2022
## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10
2123

opentelemetry-sdk/src/opentelemetry/sdk/_metrics/export/__init__.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
from enum import Enum
1919
from os import environ, linesep
2020
from sys import stdout
21-
from threading import Event, Thread
22-
from typing import IO, Callable, Iterable, Optional, Sequence
21+
from threading import Event, RLock, Thread
22+
from typing import IO, Callable, Iterable, List, Optional, Sequence
2323

2424
from opentelemetry.context import (
2525
_SUPPRESS_INSTRUMENTATION_KEY,
@@ -96,6 +96,36 @@ def shutdown(self) -> None:
9696
pass
9797

9898

99+
class InMemoryMetricReader(MetricReader):
100+
"""Implementation of :class:`MetricReader` that returns its metrics from :func:`metrics`.
101+
102+
This is useful for e.g. unit tests.
103+
"""
104+
105+
def __init__(
106+
self,
107+
preferred_temporality: AggregationTemporality = AggregationTemporality.CUMULATIVE,
108+
) -> None:
109+
super().__init__(preferred_temporality=preferred_temporality)
110+
self._lock = RLock()
111+
self._metrics: List[Metric] = []
112+
113+
def get_metrics(self) -> List[Metric]:
114+
"""Reads and returns current metrics from the SDK"""
115+
with self._lock:
116+
self.collect()
117+
metrics = self._metrics
118+
self._metrics = []
119+
return metrics
120+
121+
def _receive_metrics(self, metrics: Iterable[Metric]):
122+
with self._lock:
123+
self._metrics = list(metrics)
124+
125+
def shutdown(self) -> bool:
126+
return True
127+
128+
99129
class PeriodicExportingMetricReader(MetricReader):
100130
"""`PeriodicExportingMetricReader` is an implementation of `MetricReader`
101131
that collects metrics based on a user-configurable time interval, and passes the
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from unittest import TestCase
16+
from unittest.mock import Mock
17+
18+
from opentelemetry._metrics.measurement import Measurement
19+
from opentelemetry.sdk._metrics import MeterProvider
20+
from opentelemetry.sdk._metrics.export import InMemoryMetricReader
21+
from opentelemetry.sdk._metrics.point import (
22+
AggregationTemporality,
23+
Metric,
24+
Sum,
25+
)
26+
from opentelemetry.sdk.resources import Resource
27+
from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
28+
29+
30+
class TestInMemoryMetricReader(TestCase):
31+
def test_no_metrics(self):
32+
mock_collect_callback = Mock(return_value=[])
33+
reader = InMemoryMetricReader()
34+
reader._set_collect_callback(mock_collect_callback)
35+
self.assertEqual(reader.get_metrics(), [])
36+
mock_collect_callback.assert_called_once()
37+
38+
def test_converts_metrics_to_list(self):
39+
metric = Metric(
40+
attributes={"myattr": "baz"},
41+
description="",
42+
instrumentation_info=InstrumentationInfo("testmetrics"),
43+
name="foo",
44+
resource=Resource.create(),
45+
unit="",
46+
point=Sum(
47+
start_time_unix_nano=1647626444152947792,
48+
time_unix_nano=1647626444153163239,
49+
value=72.3309814450449,
50+
aggregation_temporality=AggregationTemporality.CUMULATIVE,
51+
is_monotonic=True,
52+
),
53+
)
54+
mock_collect_callback = Mock(return_value=(metric,))
55+
reader = InMemoryMetricReader()
56+
reader._set_collect_callback(mock_collect_callback)
57+
58+
returned_metrics = reader.get_metrics()
59+
mock_collect_callback.assert_called_once()
60+
self.assertIsInstance(returned_metrics, list)
61+
self.assertEqual(len(returned_metrics), 1)
62+
self.assertIs(returned_metrics[0], metric)
63+
64+
def test_shutdown(self):
65+
# shutdown should always be successful
66+
self.assertTrue(InMemoryMetricReader().shutdown())
67+
68+
def test_integration(self):
69+
reader = InMemoryMetricReader()
70+
meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter")
71+
counter1 = meter.create_counter("counter1")
72+
meter.create_observable_gauge(
73+
"observable_gauge1", lambda: [Measurement(value=12)]
74+
)
75+
counter1.add(1, {"foo": "1"})
76+
counter1.add(1, {"foo": "2"})
77+
78+
metrics = reader.get_metrics()
79+
# should be 3 metrics, one from the observable gauge and one for each labelset from the counter
80+
self.assertEqual(len(metrics), 3)

0 commit comments

Comments
 (0)