Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 83e6e17

Browse files
srikanthccvaabmass
andauthored
Add periodic exporting metric reader (open-telemetry#2340)
* Add periodic exporting metric reader * Fix lint * Fix typing and env * lint: black format * formatting * lot of changes * Fix lint * Lint again * Pylint fix * Review suggestions * fix failures * Address review suggestions * Once wrapper, set callback, refactor, and more * Apply suggestions from code review Co-authored-by: Aaron Abbott <[email protected]> * Add preferred_temporality to exporter; update doc string * Fix lint Co-authored-by: Aaron Abbott <[email protected]>
1 parent 6bdc5fd commit 83e6e17

File tree

4 files changed

+281
-5
lines changed

4 files changed

+281
-5
lines changed

opentelemetry-sdk/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ install_requires =
4747
opentelemetry-semantic-conventions == 0.27b0
4848
setuptools >= 16.0
4949
dataclasses == 0.8; python_version < '3.7'
50+
typing-extensions >= 3.7.4
5051

5152
[options.packages.find]
5253
where = src

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

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,26 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import logging
16+
import os
1517
from abc import ABC, abstractmethod
1618
from enum import Enum
17-
from os import linesep
19+
from os import environ, linesep
1820
from sys import stdout
19-
from typing import IO, Callable, Sequence
21+
from threading import Event, Thread
22+
from typing import IO, Callable, Iterable, Optional, Sequence
2023

21-
from opentelemetry.sdk._metrics.point import Metric
24+
from opentelemetry.context import (
25+
_SUPPRESS_INSTRUMENTATION_KEY,
26+
attach,
27+
detach,
28+
set_value,
29+
)
30+
from opentelemetry.sdk._metrics.metric_reader import MetricReader
31+
from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric
32+
from opentelemetry.util._once import Once
33+
34+
_logger = logging.getLogger(__name__)
2235

2336

2437
class MetricExportResult(Enum):
@@ -33,6 +46,10 @@ class MetricExporter(ABC):
3346
in their own format.
3447
"""
3548

49+
@property
50+
def preferred_temporality(self) -> AggregationTemporality:
51+
return AggregationTemporality.CUMULATIVE
52+
3653
@abstractmethod
3754
def export(self, metrics: Sequence[Metric]) -> "MetricExportResult":
3855
"""Exports a batch of telemetry data.
@@ -77,3 +94,82 @@ def export(self, metrics: Sequence[Metric]) -> MetricExportResult:
7794

7895
def shutdown(self) -> None:
7996
pass
97+
98+
99+
class PeriodicExportingMetricReader(MetricReader):
100+
"""`PeriodicExportingMetricReader` is an implementation of `MetricReader`
101+
that collects metrics based on a user-configurable time interval, and passes the
102+
metrics to the configured exporter.
103+
"""
104+
105+
def __init__(
106+
self,
107+
exporter: MetricExporter,
108+
export_interval_millis: Optional[float] = None,
109+
export_timeout_millis: Optional[float] = None,
110+
) -> None:
111+
super().__init__(preferred_temporality=exporter.preferred_temporality)
112+
self._exporter = exporter
113+
if export_interval_millis is None:
114+
try:
115+
export_interval_millis = float(
116+
environ.get("OTEL_METRIC_EXPORT_INTERVAL", 60000)
117+
)
118+
except ValueError:
119+
_logger.warning(
120+
"Found invalid value for export interval, using default"
121+
)
122+
export_interval_millis = 60000
123+
if export_timeout_millis is None:
124+
try:
125+
export_timeout_millis = float(
126+
environ.get("OTEL_METRIC_EXPORT_TIMEOUT", 30000)
127+
)
128+
except ValueError:
129+
_logger.warning(
130+
"Found invalid value for export timeout, using default"
131+
)
132+
export_timeout_millis = 30000
133+
self._export_interval_millis = export_interval_millis
134+
self._export_timeout_millis = export_timeout_millis
135+
self._shutdown = False
136+
self._shutdown_event = Event()
137+
self._shutdown_once = Once()
138+
self._daemon_thread = Thread(target=self._ticker, daemon=True)
139+
self._daemon_thread.start()
140+
if hasattr(os, "register_at_fork"):
141+
os.register_at_fork(
142+
after_in_child=self._at_fork_reinit
143+
) # pylint: disable=protected-access
144+
145+
def _at_fork_reinit(self):
146+
self._daemon_thread = Thread(target=self._ticker, daemon=True)
147+
self._daemon_thread.start()
148+
149+
def _ticker(self) -> None:
150+
interval_secs = self._export_interval_millis / 1e3
151+
while not self._shutdown_event.wait(interval_secs):
152+
self.collect()
153+
# one last collection below before shutting down completely
154+
self.collect()
155+
156+
def _receive_metrics(self, metrics: Iterable[Metric]) -> None:
157+
token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True))
158+
try:
159+
self._exporter.export(metrics)
160+
except Exception as e: # pylint: disable=broad-except,invalid-name
161+
_logger.exception("Exception while exporting metrics %s", str(e))
162+
detach(token)
163+
164+
def shutdown(self) -> bool:
165+
def _shutdown():
166+
self._shutdown = True
167+
168+
did_set = self._shutdown_once.do_once(_shutdown)
169+
if not did_set:
170+
_logger.warning("Can't shutdown multiple times")
171+
return False
172+
173+
self._shutdown_event.set()
174+
self._daemon_thread.join()
175+
return self._exporter.shutdown()

opentelemetry-sdk/src/opentelemetry/sdk/_metrics/metric_reader.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,59 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import logging
16+
from abc import ABC, abstractmethod
17+
from typing import Callable, Iterable
1518

16-
class MetricReader:
17-
pass
19+
from typing_extensions import final
20+
21+
from opentelemetry.sdk._metrics.point import AggregationTemporality, Metric
22+
23+
_logger = logging.getLogger(__name__)
24+
25+
26+
class MetricReader(ABC):
27+
def __init__(
28+
self,
29+
preferred_temporality: AggregationTemporality = AggregationTemporality.CUMULATIVE,
30+
) -> None:
31+
self._collect: Callable[
32+
["MetricReader", AggregationTemporality], Iterable[Metric]
33+
] = None
34+
self._preferred_temporality = preferred_temporality
35+
36+
@final
37+
def collect(self) -> None:
38+
"""Collects the metrics from the internal SDK state and
39+
invokes the `_receive_metrics` with the collection.
40+
"""
41+
if self._collect is None:
42+
_logger.warning(
43+
"Cannot call collect on a MetricReader until it is registered on a MeterProvider"
44+
)
45+
return
46+
self._receive_metrics(self._collect(self, self._preferred_temporality))
47+
48+
@final
49+
def _set_collect_callback(
50+
self,
51+
func: Callable[
52+
["MetricReader", AggregationTemporality], Iterable[Metric]
53+
],
54+
) -> None:
55+
"""This function is internal to the SDK. It should not be called or overriden by users"""
56+
self._collect = func
57+
58+
@abstractmethod
59+
def _receive_metrics(self, metrics: Iterable[Metric]):
60+
"""Called by `MetricReader.collect` when it receives a batch of metrics"""
61+
62+
@abstractmethod
63+
def shutdown(self) -> bool:
64+
"""Shuts down the MetricReader. This method provides a way
65+
for the MetricReader to do any cleanup required. A metric reader can
66+
only be shutdown once, any subsequent calls are ignored and return
67+
failure status.
68+
69+
When a `MetricReader` is registered on a `MeterProvider`, `MeterProvider.shutdown` will invoke this automatically.
70+
"""
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
import time
16+
from unittest.mock import Mock
17+
18+
from opentelemetry.sdk._metrics.export import (
19+
MetricExporter,
20+
PeriodicExportingMetricReader,
21+
)
22+
from opentelemetry.sdk._metrics.point import Gauge, Metric, Sum
23+
from opentelemetry.sdk.resources import Resource
24+
from opentelemetry.test.concurrency_test import ConcurrencyTestBase
25+
from opentelemetry.util._time import _time_ns
26+
27+
28+
class FakeMetricsExporter(MetricExporter):
29+
def __init__(self, wait=0):
30+
self.wait = wait
31+
self.metrics = []
32+
self._shutdown = False
33+
34+
def export(self, metrics):
35+
time.sleep(self.wait)
36+
self.metrics.extend(metrics)
37+
return True
38+
39+
def shutdown(self):
40+
self._shutdown = True
41+
42+
43+
metrics_list = [
44+
Metric(
45+
name="sum_name",
46+
attributes={},
47+
description="",
48+
instrumentation_info=None,
49+
resource=Resource.create(),
50+
unit="",
51+
point=Sum(
52+
start_time_unix_nano=_time_ns(),
53+
time_unix_nano=_time_ns(),
54+
value=2,
55+
aggregation_temporality=1,
56+
is_monotonic=True,
57+
),
58+
),
59+
Metric(
60+
name="gauge_name",
61+
attributes={},
62+
description="",
63+
instrumentation_info=None,
64+
resource=Resource.create(),
65+
unit="",
66+
point=Gauge(
67+
time_unix_nano=_time_ns(),
68+
value=2,
69+
),
70+
),
71+
]
72+
73+
74+
class TestPeriodicExportingMetricReader(ConcurrencyTestBase):
75+
def test_defaults(self):
76+
pmr = PeriodicExportingMetricReader(FakeMetricsExporter())
77+
self.assertEqual(pmr._export_interval_millis, 60000)
78+
self.assertEqual(pmr._export_timeout_millis, 30000)
79+
pmr.shutdown()
80+
81+
def _create_periodic_reader(
82+
self, metrics, exporter, collect_wait=0, interval=60000
83+
):
84+
85+
pmr = PeriodicExportingMetricReader(exporter, interval)
86+
87+
def _collect(reader, temp):
88+
time.sleep(collect_wait)
89+
pmr._receive_metrics(metrics)
90+
91+
pmr._set_collect_callback(_collect)
92+
return pmr
93+
94+
def test_ticker_called(self):
95+
collect_mock = Mock()
96+
pmr = PeriodicExportingMetricReader(Mock(), 1)
97+
pmr._set_collect_callback(collect_mock)
98+
time.sleep(0.1)
99+
self.assertTrue(collect_mock.assert_called_once)
100+
pmr.shutdown()
101+
102+
def test_ticker_collects_metrics(self):
103+
exporter = FakeMetricsExporter()
104+
105+
pmr = self._create_periodic_reader(
106+
metrics_list, exporter, interval=100
107+
)
108+
time.sleep(0.2)
109+
self.assertEqual(exporter.metrics, metrics_list)
110+
pmr.shutdown()
111+
112+
def test_shutdown(self):
113+
exporter = FakeMetricsExporter()
114+
115+
pmr = self._create_periodic_reader([], exporter)
116+
pmr.shutdown()
117+
self.assertEqual(exporter.metrics, [])
118+
self.assertTrue(pmr._shutdown)
119+
self.assertTrue(exporter._shutdown)
120+
121+
def test_shutdown_multiple_times(self):
122+
pmr = self._create_periodic_reader([], Mock())
123+
with self.assertLogs(level="WARNING") as w:
124+
self.run_with_many_threads(pmr.shutdown)
125+
self.assertTrue("Can't shutdown multiple times", w.output[0])
126+
pmr.shutdown()

0 commit comments

Comments
 (0)