Skip to content

Commit

Permalink
Make Measurement a concrete class
Browse files Browse the repository at this point in the history
  • Loading branch information
aabmass committed Sep 24, 2021
1 parent 613ff32 commit f3d75ec
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 91 deletions.
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
nitpick_ignore = [
("py:class", "ValueT"),
("py:class", "MetricT"),
("py:obj", "opentelemetry.metrics.measurement.ValueT"),
# Even if wrapt is added to intersphinx_mapping, sphinx keeps failing
# with "class reference target not found: ObjectProxy".
("py:class", "ObjectProxy"),
Expand Down
6 changes: 3 additions & 3 deletions opentelemetry-api/src/opentelemetry/metrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def create_observable_counter(
For example, an observable counter could be used to report system CPU
time periodically. Here is a basic implementation::
def cpu_time_callback() -> Iterable[Measurement]:
def cpu_time_callback() -> Iterable[Measurement[int]]:
measurements = []
with open("/proc/stat") as procstat:
procstat.readline() # skip the first line
Expand All @@ -159,7 +159,7 @@ def cpu_time_callback() -> Iterable[Measurement]:
To reduce memory usage, you can use generator callbacks instead of
building the full list::
def cpu_time_callback() -> Iterable[Measurement]:
def cpu_time_callback() -> Iterable[Measurement[int]]:
with open("/proc/stat") as procstat:
procstat.readline() # skip the first line
for line in procstat:
Expand All @@ -173,7 +173,7 @@ def cpu_time_callback() -> Iterable[Measurement]:
which should return iterables of
:class:`~opentelemetry.metrics.measurement.Measurement`::
def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurement]]:
def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Measurement[int]]]:
while True:
measurements = []
with open("/proc/stat") as procstat:
Expand Down
39 changes: 23 additions & 16 deletions opentelemetry-api/src/opentelemetry/metrics/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@
from collections import abc as collections_abc
from logging import getLogger
from re import compile as compile_
from typing import Callable, Generator, Iterable, Union
from typing import Callable, Generator, Generic, Iterable, Union

from opentelemetry.metrics.measurement import Measurement
from opentelemetry.metrics.measurement import Measurement, ValueT

_TInstrumentCallback = Callable[[], Iterable[Measurement]]
_TInstrumentCallbackGenerator = Generator[Iterable[Measurement], None, None]
TCallback = Union[_TInstrumentCallback, _TInstrumentCallbackGenerator]
_InstrumentCallbackT = Callable[[], Iterable[Measurement[ValueT]]]
_InstrumentCallbackGeneratorT = Generator[
Iterable[Measurement[ValueT]], None, None
]
CallbackT = Union[
_InstrumentCallbackT[ValueT], _InstrumentCallbackGeneratorT[ValueT]
]


_logger = getLogger(__name__)
Expand Down Expand Up @@ -77,12 +81,12 @@ class Synchronous(Instrument):
pass


class Asynchronous(Instrument):
class Asynchronous(Generic[ValueT], Instrument):
@abstractmethod
def __init__(
self,
name,
callback: TCallback,
callback: CallbackT[ValueT],
*args,
unit="",
description="",
Expand All @@ -101,12 +105,12 @@ def __init__(

def _wrap_generator_callback(
self,
generator_callback: _TInstrumentCallbackGenerator,
) -> _TInstrumentCallback:
generator_callback: _InstrumentCallbackGeneratorT[ValueT],
) -> _InstrumentCallbackT[ValueT]:
"""Wraps a generator style callback into a callable one"""
has_items = True

def inner() -> Iterable[Measurement]:
def inner() -> Iterable[Measurement[ValueT]]:
nonlocal has_items
if not has_items:
return []
Expand Down Expand Up @@ -186,7 +190,7 @@ def add(self, amount, attributes=None):
return super().add(amount, attributes=attributes)


class ObservableCounter(_Monotonic, Asynchronous):
class ObservableCounter(Asynchronous[ValueT], _Monotonic):
def callback(self):
measurements = super().callback()

Expand All @@ -196,17 +200,20 @@ def callback(self):
yield measurement


class DefaultObservableCounter(ObservableCounter):
class DefaultObservableCounter(ObservableCounter[ValueT]):
def __init__(self, name, callback, unit="", description=""):
super().__init__(name, callback, unit=unit, description=description)


class ObservableUpDownCounter(_NonMonotonic, Asynchronous):
class ObservableUpDownCounter(
Asynchronous[ValueT],
_NonMonotonic,
):

pass


class DefaultObservableUpDownCounter(ObservableUpDownCounter):
class DefaultObservableUpDownCounter(ObservableUpDownCounter[ValueT]):
def __init__(self, name, callback, unit="", description=""):
super().__init__(name, callback, unit=unit, description=description)

Expand All @@ -225,10 +232,10 @@ def record(self, amount, attributes=None):
return super().record(amount, attributes=attributes)


class ObservableGauge(_Grouping, Asynchronous):
class ObservableGauge(Asynchronous[ValueT], _Grouping):
pass


class DefaultObservableGauge(ObservableGauge):
class DefaultObservableGauge(ObservableGauge[ValueT]):
def __init__(self, name, callback, unit="", description=""):
super().__init__(name, callback, unit=unit, description=description)
40 changes: 26 additions & 14 deletions opentelemetry-api/src/opentelemetry/metrics/measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,40 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# pylint: disable=too-many-ancestors
# type:ignore

from typing import Generic, TypeVar

from abc import ABC, abstractmethod
from opentelemetry.util.types import Attributes

ValueT = TypeVar("ValueT", int, float)


class Measurement(Generic[ValueT]):
"""A measurement observed in an asynchronous instrument
Return/yield instances of this class from asynchronous instrument callbacks.
Args:
value: The float or int measured value
attributes: The measurement's attributes
"""

def __init__(self, value: ValueT, attributes: Attributes = None) -> None:
self._value = value
self._attributes = attributes

class Measurement(ABC):
@property
def value(self):
def value(self) -> ValueT:
return self._value

@property
def attributes(self):
def attributes(self) -> Attributes:
return self._attributes

@abstractmethod
def __init__(self, value, attributes=None):
self._value = value
self._attributes = attributes

def __eq__(self, other: "Measurement[ValueT]") -> bool:
return (
self.value == other.value and self.attributes == other.attributes
)

class DefaultMeasurement(Measurement):
def __init__(self, value, attributes=None):
super().__init__(value, attributes=attributes)
def __repr__(self) -> str:
return f"Measurement(value={self.value}, attributes={self.attributes})"
84 changes: 38 additions & 46 deletions opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@
# FIXME Test that the instrument methods can be called concurrently safely.


class ChildMeasurement(Measurement):
def __init__(self, value, attributes=None):
super().__init__(value, attributes=attributes)

def __eq__(self, o: Measurement) -> bool:
return self.value == o.value and self.attributes == o.attributes


class TestCpuTimeIntegration(TestCase):
"""Integration test of scraping CPU time from proc stat with an observable
counter"""
Expand All @@ -48,61 +40,61 @@ class TestCpuTimeIntegration(TestCase):
softirq 1644603067 0 166540056 208 309152755 8936439 0 1354908 935642970 13 222975718\n"""

measurements_expected = [
ChildMeasurement(6150, {"cpu": "cpu0", "state": "user"}),
ChildMeasurement(3177, {"cpu": "cpu0", "state": "nice"}),
ChildMeasurement(5946, {"cpu": "cpu0", "state": "system"}),
ChildMeasurement(891264, {"cpu": "cpu0", "state": "idle"}),
ChildMeasurement(1296, {"cpu": "cpu0", "state": "iowait"}),
ChildMeasurement(0, {"cpu": "cpu0", "state": "irq"}),
ChildMeasurement(8343, {"cpu": "cpu0", "state": "softirq"}),
ChildMeasurement(421, {"cpu": "cpu0", "state": "guest"}),
ChildMeasurement(0, {"cpu": "cpu0", "state": "guest_nice"}),
ChildMeasurement(5882, {"cpu": "cpu1", "state": "user"}),
ChildMeasurement(3491, {"cpu": "cpu1", "state": "nice"}),
ChildMeasurement(6404, {"cpu": "cpu1", "state": "system"}),
ChildMeasurement(891564, {"cpu": "cpu1", "state": "idle"}),
ChildMeasurement(1244, {"cpu": "cpu1", "state": "iowait"}),
ChildMeasurement(0, {"cpu": "cpu1", "state": "irq"}),
ChildMeasurement(2410, {"cpu": "cpu1", "state": "softirq"}),
ChildMeasurement(418, {"cpu": "cpu1", "state": "guest"}),
ChildMeasurement(0, {"cpu": "cpu1", "state": "guest_nice"}),
Measurement(6150, {"cpu": "cpu0", "state": "user"}),
Measurement(3177, {"cpu": "cpu0", "state": "nice"}),
Measurement(5946, {"cpu": "cpu0", "state": "system"}),
Measurement(891264, {"cpu": "cpu0", "state": "idle"}),
Measurement(1296, {"cpu": "cpu0", "state": "iowait"}),
Measurement(0, {"cpu": "cpu0", "state": "irq"}),
Measurement(8343, {"cpu": "cpu0", "state": "softirq"}),
Measurement(421, {"cpu": "cpu0", "state": "guest"}),
Measurement(0, {"cpu": "cpu0", "state": "guest_nice"}),
Measurement(5882, {"cpu": "cpu1", "state": "user"}),
Measurement(3491, {"cpu": "cpu1", "state": "nice"}),
Measurement(6404, {"cpu": "cpu1", "state": "system"}),
Measurement(891564, {"cpu": "cpu1", "state": "idle"}),
Measurement(1244, {"cpu": "cpu1", "state": "iowait"}),
Measurement(0, {"cpu": "cpu1", "state": "irq"}),
Measurement(2410, {"cpu": "cpu1", "state": "softirq"}),
Measurement(418, {"cpu": "cpu1", "state": "guest"}),
Measurement(0, {"cpu": "cpu1", "state": "guest_nice"}),
]

def test_cpu_time_callback(self):
meter = _DefaultMeter("foo")

def cpu_time_callback() -> Iterable[Measurement]:
def cpu_time_callback() -> Iterable[Measurement[int]]:
procstat = io.StringIO(self.procstat_str)
procstat.readline() # skip the first line
for line in procstat:
if not line.startswith("cpu"):
break
cpu, *states = line.split()
yield ChildMeasurement(
yield Measurement(
int(states[0]) // 100, {"cpu": cpu, "state": "user"}
)
yield ChildMeasurement(
yield Measurement(
int(states[1]) // 100, {"cpu": cpu, "state": "nice"}
)
yield ChildMeasurement(
yield Measurement(
int(states[2]) // 100, {"cpu": cpu, "state": "system"}
)
yield ChildMeasurement(
yield Measurement(
int(states[3]) // 100, {"cpu": cpu, "state": "idle"}
)
yield ChildMeasurement(
yield Measurement(
int(states[4]) // 100, {"cpu": cpu, "state": "iowait"}
)
yield ChildMeasurement(
yield Measurement(
int(states[5]) // 100, {"cpu": cpu, "state": "irq"}
)
yield ChildMeasurement(
yield Measurement(
int(states[6]) // 100, {"cpu": cpu, "state": "softirq"}
)
yield ChildMeasurement(
yield Measurement(
int(states[7]) // 100, {"cpu": cpu, "state": "guest"}
)
yield ChildMeasurement(
yield Measurement(
int(states[8]) // 100, {"cpu": cpu, "state": "guest_nice"}
)

Expand All @@ -119,7 +111,7 @@ def test_cpu_time_generator(self):
meter = _DefaultMeter("foo")

def cpu_time_generator() -> Generator[
Iterable[Measurement], None, None
Iterable[Measurement[int]], None, None
]:
while True:
measurements = []
Expand All @@ -130,54 +122,54 @@ def cpu_time_generator() -> Generator[
break
cpu, *states = line.split()
measurements.append(
ChildMeasurement(
Measurement(
int(states[0]) // 100,
{"cpu": cpu, "state": "user"},
)
)
measurements.append(
ChildMeasurement(
Measurement(
int(states[1]) // 100,
{"cpu": cpu, "state": "nice"},
)
)
measurements.append(
ChildMeasurement(
Measurement(
int(states[2]) // 100,
{"cpu": cpu, "state": "system"},
)
)
measurements.append(
ChildMeasurement(
Measurement(
int(states[3]) // 100,
{"cpu": cpu, "state": "idle"},
)
)
measurements.append(
ChildMeasurement(
Measurement(
int(states[4]) // 100,
{"cpu": cpu, "state": "iowait"},
)
)
measurements.append(
ChildMeasurement(
Measurement(
int(states[5]) // 100, {"cpu": cpu, "state": "irq"}
)
)
measurements.append(
ChildMeasurement(
Measurement(
int(states[6]) // 100,
{"cpu": cpu, "state": "softirq"},
)
)
measurements.append(
ChildMeasurement(
Measurement(
int(states[7]) // 100,
{"cpu": cpu, "state": "guest"},
)
)
measurements.append(
ChildMeasurement(
Measurement(
int(states[8]) // 100,
{"cpu": cpu, "state": "guest_nice"},
)
Expand Down
Loading

0 comments on commit f3d75ec

Please sign in to comment.