diff --git a/docs/conf.py b/docs/conf.py index 6559298b8ed..9fa6906ea8e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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"), diff --git a/opentelemetry-api/src/opentelemetry/metrics/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/__init__.py index 83d210e063b..43706fb6686 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/metrics/__init__.py @@ -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 @@ -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: @@ -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: diff --git a/opentelemetry-api/src/opentelemetry/metrics/instrument.py b/opentelemetry-api/src/opentelemetry/metrics/instrument.py index 5d382056408..554298777ed 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/metrics/instrument.py @@ -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__) @@ -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="", @@ -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 [] @@ -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() @@ -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) @@ -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) diff --git a/opentelemetry-api/src/opentelemetry/metrics/measurement.py b/opentelemetry-api/src/opentelemetry/metrics/measurement.py index 6b5b081c266..73977283a58 100644 --- a/opentelemetry-api/src/opentelemetry/metrics/measurement.py +++ b/opentelemetry-api/src/opentelemetry/metrics/measurement.py @@ -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})" diff --git a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py index 347f6c4dc48..bb8e20107ee 100644 --- a/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py +++ b/opentelemetry-api/tests/metrics/integration_test/test_cpu_time.py @@ -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""" @@ -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"} ) @@ -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 = [] @@ -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"}, ) diff --git a/opentelemetry-api/tests/metrics/test_instruments.py b/opentelemetry-api/tests/metrics/test_instruments.py index 2dd100c9ed7..9be227ec6f6 100644 --- a/opentelemetry-api/tests/metrics/test_instruments.py +++ b/opentelemetry-api/tests/metrics/test_instruments.py @@ -45,11 +45,6 @@ def __init__(self, name, *args, unit="", description="", **kwargs): ) -class ChildMeasurement(Measurement): - def __init__(self, value, attributes=None): - super().__init__(value, attributes=attributes) - - class TestInstrument(TestCase): def test_instrument_has_name(self): """ @@ -341,8 +336,8 @@ def callback(): list(observable_counter.callback()) def callback(): - yield [ChildMeasurement(1), ChildMeasurement(2)] - yield [ChildMeasurement(-1)] + yield [Measurement(1), Measurement(2)] + yield [Measurement(-1)] observable_counter = DefaultObservableCounter("name", callback()) @@ -382,7 +377,7 @@ def callback_invalid_return(): list(observable_counter.callback()) def callback_valid(): - return [ChildMeasurement(1), ChildMeasurement(2)] + return [Measurement(1), Measurement(2)] observable_counter = DefaultObservableCounter("name", callback_valid) @@ -391,7 +386,7 @@ def callback_valid(): list(observable_counter.callback()) def callback_one_invalid(): - return [ChildMeasurement(1), ChildMeasurement(-2)] + return [Measurement(1), Measurement(-2)] observable_counter = DefaultObservableCounter( "name", callback_one_invalid @@ -578,7 +573,7 @@ def callback(): list(observable_gauge.callback()) def callback(): - yield [ChildMeasurement(1), ChildMeasurement(-1)] + yield [Measurement(1), Measurement(-1)] observable_gauge = DefaultObservableGauge("name", callback()) with self.assertRaises(AssertionError): @@ -786,8 +781,8 @@ def test_observable_up_down_counter_callback(self): ) def callback(): - yield ChildMeasurement(1) - yield ChildMeasurement(-1) + yield Measurement(1) + yield Measurement(-1) with self.assertRaises(AssertionError): with self.assertLogs(level=ERROR): diff --git a/opentelemetry-api/tests/metrics/test_measurement.py b/opentelemetry-api/tests/metrics/test_measurement.py new file mode 100644 index 00000000000..7feff7210f3 --- /dev/null +++ b/opentelemetry-api/tests/metrics/test_measurement.py @@ -0,0 +1,41 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry.metrics.measurement import Measurement + + +class TestMeasurement(TestCase): + def test_measurement_init(self): + # int + Measurement(321, {"hello": "world"}) + + # float + Measurement(321.321, {"hello": "world"}) + + def test_measurement_equality(self): + self.assertEqual( + Measurement(321, {"hello": "world"}), + Measurement(321, {"hello": "world"}), + ) + + self.assertNotEqual( + Measurement(321, {"hello": "world"}), + Measurement(321.321, {"hello": "world"}), + ) + self.assertNotEqual( + Measurement(321, {"baz": "world"}), + Measurement(321, {"hello": "world"}), + )