From eec8162b3ee010dc7729ae33279a1789c785f060 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 20 Apr 2022 10:34:26 -0600 Subject: [PATCH] Make necessary metrics API symbols private Fixes #2622 --- docs/api/metrics.instrument.rst | 2 +- docs/api/metrics.observation.rst | 2 +- docs/api/metrics.rst | 4 +- .../src/opentelemetry/_metrics/___init__.py | 671 +++++++++++++++++ .../src/opentelemetry/_metrics/__init__.py | 672 +----------------- .../src/opentelemetry/_metrics/_instrument.py | 277 ++++++++ .../opentelemetry/_metrics/_observation.py | 52 ++ .../src/opentelemetry/_metrics/instrument.py | 294 +------- .../src/opentelemetry/_metrics/observation.py | 38 +- .../tests/metrics/test_meter_provider.py | 12 +- .../opentelemetry/sdk/_metrics/__init__.py | 2 +- .../opentelemetry/sdk/_metrics/aggregation.py | 30 +- .../src/opentelemetry/test/globals_test.py | 2 +- 13 files changed, 1085 insertions(+), 973 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/_metrics/___init__.py create mode 100644 opentelemetry-api/src/opentelemetry/_metrics/_instrument.py create mode 100644 opentelemetry-api/src/opentelemetry/_metrics/_observation.py diff --git a/docs/api/metrics.instrument.rst b/docs/api/metrics.instrument.rst index 7d23f6fa773..e1987a3cbbd 100644 --- a/docs/api/metrics.instrument.rst +++ b/docs/api/metrics.instrument.rst @@ -1,7 +1,7 @@ opentelemetry._metrics.instrument ================================= -.. automodule:: opentelemetry._metrics.instrument +.. automodule:: opentelemetry._metrics._instrument :members: :private-members: :undoc-members: diff --git a/docs/api/metrics.observation.rst b/docs/api/metrics.observation.rst index 3df89ae6a5e..c888f0cc57f 100644 --- a/docs/api/metrics.observation.rst +++ b/docs/api/metrics.observation.rst @@ -1,7 +1,7 @@ opentelemetry._metrics.observation ================================== -.. automodule:: opentelemetry._metrics.observation +.. automodule:: opentelemetry._metrics._observation :members: :undoc-members: :show-inheritance: diff --git a/docs/api/metrics.rst b/docs/api/metrics.rst index 08c4cbcc708..0d91cf1dc6c 100644 --- a/docs/api/metrics.rst +++ b/docs/api/metrics.rst @@ -3,7 +3,7 @@ opentelemetry._metrics package .. warning:: OpenTelemetry Python metrics are in an experimental state. The APIs within - :mod:`opentelemetry._metrics` are subject to change in minor/patch releases and make no + :mod:`opentelemetry._metrics.___init__` are subject to change in minor/patch releases and make no backward compatability guarantees at this time. Once metrics become stable, this package will be be renamed to ``opentelemetry.metrics``. @@ -19,4 +19,4 @@ Submodules Module contents --------------- -.. automodule:: opentelemetry._metrics +.. automodule:: opentelemetry._metrics.___init__ diff --git a/opentelemetry-api/src/opentelemetry/_metrics/___init__.py b/opentelemetry-api/src/opentelemetry/_metrics/___init__.py new file mode 100644 index 00000000000..65ba89656e0 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/_metrics/___init__.py @@ -0,0 +1,671 @@ +# 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. + +# pylint: disable=too-many-ancestors +# type: ignore + +""" +The OpenTelemetry metrics API describes the classes used to generate +metrics. + +The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in +turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are +used to record measurements. + +This module provides abstract (i.e. unimplemented) classes required for +metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications +to use the API package alone without a supporting implementation. + +To get a meter, you need to provide the package name from which you are +calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter` +with the calling instrumentation name and the version of your package. + +The following code shows how to obtain a meter using the global :class:`.MeterProvider`:: + + from opentelemetry._metrics import get_meter + + meter = get_meter("example-meter") + counter = meter.create_counter("example-counter") + +.. versionadded:: 1.10.0 +""" + + +from abc import ABC, abstractmethod +from logging import getLogger +from os import environ +from threading import Lock +from typing import List, Optional, Set, cast + +from opentelemetry._metrics._instrument import ( + Counter, + Histogram, + NoOpCounter, + NoOpHistogram, + NoOpObservableCounter, + NoOpObservableGauge, + NoOpObservableUpDownCounter, + NoOpUpDownCounter, + ObservableCounter, + ObservableGauge, + ObservableUpDownCounter, + UpDownCounter, + _ProxyCounter, + _ProxyHistogram, + _ProxyInstrument, + _ProxyObservableCounter, + _ProxyObservableGauge, + _ProxyObservableUpDownCounter, + _ProxyUpDownCounter, +) +from opentelemetry.environment_variables import ( + _OTEL_PYTHON_METER_PROVIDER as OTEL_PYTHON_METER_PROVIDER, +) +from opentelemetry.util._once import Once +from opentelemetry.util._providers import _load_provider + +_logger = getLogger(__name__) + + +class MeterProvider(ABC): + """ + MeterProvider is the entry point of the API. It provides access to `Meter` instances. + """ + + @abstractmethod + def get_meter( + self, + name: str, + version: str = None, + schema_url: str = None, + ) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + + For any two calls it is undefined whether the same or different + `Meter` instances are returned, even for different library names. + + This function may return different `Meter` types (e.g. a no-op meter + vs. a functional meter). + + Args: + name: The name of the instrumenting module. + ``__name__`` may not be used as this can result in + different meter names if the meters are in different files. + It is better to use a fixed string that can be imported where + needed and used consistently as the name of the meter. + + This should *not* be the name of the module that is + instrumented but the name of the module doing the instrumentation. + E.g., instead of ``"requests"``, use + ``"opentelemetry.instrumentation.requests"``. + + version: Optional. The version string of the + instrumenting library. Usually this should be the same as + ``pkg_resources.get_distribution(instrumenting_library_name).version``. + + schema_url: Optional. Specifies the Schema URL of the emitted telemetry. + """ + + +class NoOpMeterProvider(MeterProvider): + """The default MeterProvider used when no MeterProvider implementation is available.""" + + def get_meter( + self, + name, + version=None, + schema_url=None, + ) -> "Meter": + """Returns a NoOpMeter.""" + super().get_meter(name, version=version, schema_url=schema_url) + return NoOpMeter(name, version=version, schema_url=schema_url) + + +class _ProxyMeterProvider(MeterProvider): + def __init__(self) -> None: + self._lock = Lock() + self._meters: List[_ProxyMeter] = [] + self._real_meter_provider: Optional[MeterProvider] = None + + def get_meter( + self, + name, + version=None, + schema_url=None, + ) -> "Meter": + with self._lock: + if self._real_meter_provider is not None: + return self._real_meter_provider.get_meter( + name, version, schema_url + ) + + meter = _ProxyMeter(name, version=version, schema_url=schema_url) + self._meters.append(meter) + return meter + + def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: + with self._lock: + self._real_meter_provider = meter_provider + for meter in self._meters: + meter.on_set_meter_provider(meter_provider) + + +class Meter(ABC): + """Handles instrument creation. + + This class provides methods for creating instruments which are then + used to produce measurements. + """ + + def __init__(self, name: str, version: str = None, schema_url: str = None): + super().__init__() + self._name = name + self._version = version + self._schema_url = schema_url + self._instrument_ids: Set[str] = set() + self._instrument_ids_lock = Lock() + + @property + def name(self): + """ + The name of the instrumenting module. + """ + return self._name + + @property + def version(self): + """ + The version string of the instrumenting library. + """ + return self._version + + @property + def schema_url(self): + """ + Specifies the Schema URL of the emitted telemetry + """ + return self._schema_url + + def _check_instrument_id( + self, name: str, type_: type, unit: str, description: str + ) -> bool: + """ + Check if an instrument with the same name, type, unit and description + has been registered already. + """ + + result = False + + instrument_id = ",".join( + [name.strip().lower(), type_.__name__, unit, description] + ) + + with self._instrument_ids_lock: + if instrument_id in self._instrument_ids: + result = True + else: + self._instrument_ids.add(instrument_id) + + return result + + @abstractmethod + def create_counter( + self, name: str, unit: str = "", description: str = "" + ) -> Counter: + """Creates a `Counter` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_up_down_counter( + self, name: str, unit: str = "", description: str = "" + ) -> UpDownCounter: + """Creates an `UpDownCounter` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_observable_counter( + self, name, callbacks=None, unit="", description="" + ) -> ObservableCounter: + """Creates an `ObservableCounter` instrument + + An observable counter observes a monotonically increasing count by + calling provided callbacks which returns multiple + :class:`~opentelemetry._metrics._observation.Observation`. + + For example, an observable counter could be used to report system CPU + time periodically. Here is a basic implementation:: + + def cpu_time_callback() -> Iterable[Observation]: + observations = [] + with open("/proc/stat") as procstat: + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): break + cpu, *states = line.split() + observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) + observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) + observations.append(Observation(int(states[2]) // 100, {"cpu": cpu, "state": "system"})) + # ... other states + return observations + + meter.create_observable_counter( + "system.cpu.time", + callbacks=[cpu_time_callback], + unit="s", + description="CPU time" + ) + + To reduce memory usage, you can use generator callbacks instead of + building the full list:: + + def cpu_time_callback() -> Iterable[Observation]: + with open("/proc/stat") as procstat: + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): break + cpu, *states = line.split() + yield Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"}) + yield Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}) + # ... other states + + Alternatively, you can pass a sequence of generators directly instead + of a sequence of callbacks, which each should return iterables of + :class:`~opentelemetry._metrics._observation.Observation`:: + + def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]: + while True: + observations = [] + with open("/proc/stat") as procstat: + procstat.readline() # skip the first line + for line in procstat: + if not line.startswith("cpu"): break + cpu, *states = line.split() + if "user" in states_to_include: + observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) + if "nice" in states_to_include: + observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) + # ... other states + yield observations + + meter.create_observable_counter( + "system.cpu.time", + callbacks=[cpu_time_callback({"user", "system"})], + unit="s", + description="CPU time" + ) + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry._metrics._observation.Observation`. + Alternatively, can be a sequence of generators that each yields + iterables of + :class:`~opentelemetry._metrics._observation.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_histogram(self, name, unit="", description="") -> Histogram: + """Creates a `opentelemetry._metrics._instrument.Histogram` instrument + + Args: + name: The name of the instrument to be created + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_observable_gauge( + self, name, callbacks=None, unit="", description="" + ) -> ObservableGauge: + """Creates an `ObservableGauge` instrument + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry._metrics._observation.Observation`. + Alternatively, can be a generator that yields iterables of + :class:`~opentelemetry._metrics._observation.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + @abstractmethod + def create_observable_up_down_counter( + self, name, callbacks=None, unit="", description="" + ) -> ObservableUpDownCounter: + """Creates an `ObservableUpDownCounter` instrument + + Args: + name: The name of the instrument to be created + callbacks: A sequence of callbacks that return an iterable of + :class:`~opentelemetry._metrics._observation.Observation`. + Alternatively, can be a generator that yields iterables of + :class:`~opentelemetry._metrics._observation.Observation`. + unit: The unit for observations this instrument reports. For + example, ``By`` for bytes. UCUM units are recommended. + description: A description for this instrument and what it measures. + """ + + +class _ProxyMeter(Meter): + def __init__( + self, + name, + version=None, + schema_url=None, + ): + super().__init__(name, version=version, schema_url=schema_url) + self._lock = Lock() + self._instruments: List[_ProxyInstrument] = [] + self._real_meter: Optional[Meter] = None + + def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: + """Called when a real meter provider is set on the creating _ProxyMeterProvider + + Creates a real backing meter for this instance and notifies all created + instruments so they can create real backing instruments. + """ + real_meter = meter_provider.get_meter( + self._name, self._version, self._schema_url + ) + + with self._lock: + self._real_meter = real_meter + # notify all proxy instruments of the new meter so they can create + # real instruments to back themselves + for instrument in self._instruments: + instrument.on_meter_set(real_meter) + + def create_counter(self, name, unit="", description="") -> Counter: + with self._lock: + if self._real_meter: + return self._real_meter.create_counter(name, unit, description) + proxy = _ProxyCounter(name, unit, description) + self._instruments.append(proxy) + return proxy + + def create_up_down_counter( + self, name, unit="", description="" + ) -> UpDownCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_up_down_counter( + name, unit, description + ) + proxy = _ProxyUpDownCounter(name, unit, description) + self._instruments.append(proxy) + return proxy + + def create_observable_counter( + self, name, callbacks=None, unit="", description="" + ) -> ObservableCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_counter( + name, callbacks, unit, description + ) + proxy = _ProxyObservableCounter( + name, callbacks, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy + + def create_histogram(self, name, unit="", description="") -> Histogram: + with self._lock: + if self._real_meter: + return self._real_meter.create_histogram( + name, unit, description + ) + proxy = _ProxyHistogram(name, unit, description) + self._instruments.append(proxy) + return proxy + + def create_observable_gauge( + self, name, callbacks=None, unit="", description="" + ) -> ObservableGauge: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_gauge( + name, callbacks, unit, description + ) + proxy = _ProxyObservableGauge( + name, callbacks, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy + + def create_observable_up_down_counter( + self, name, callbacks=None, unit="", description="" + ) -> ObservableUpDownCounter: + with self._lock: + if self._real_meter: + return self._real_meter.create_observable_up_down_counter( + name, + callbacks, + unit, + description, + ) + proxy = _ProxyObservableUpDownCounter( + name, callbacks, unit=unit, description=description + ) + self._instruments.append(proxy) + return proxy + + +class NoOpMeter(Meter): + """The default Meter used when no Meter implementation is available. + + All operations are no-op. + """ + + def create_counter(self, name, unit="", description="") -> Counter: + """Returns a no-op Counter.""" + super().create_counter(name, unit=unit, description=description) + if self._check_instrument_id(name, NoOpCounter, unit, description): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + Counter.__name__, + unit, + description, + ) + return NoOpCounter(name, unit=unit, description=description) + + def create_up_down_counter( + self, name, unit="", description="" + ) -> UpDownCounter: + """Returns a no-op UpDownCounter.""" + super().create_up_down_counter( + name, unit=unit, description=description + ) + if self._check_instrument_id( + name, NoOpUpDownCounter, unit, description + ): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + UpDownCounter.__name__, + unit, + description, + ) + return NoOpUpDownCounter(name, unit=unit, description=description) + + def create_observable_counter( + self, name, callbacks=None, unit="", description="" + ) -> ObservableCounter: + """Returns a no-op ObservableCounter.""" + super().create_observable_counter( + name, callbacks, unit=unit, description=description + ) + if self._check_instrument_id( + name, NoOpObservableCounter, unit, description + ): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + ObservableCounter.__name__, + unit, + description, + ) + return NoOpObservableCounter( + name, + callbacks, + unit=unit, + description=description, + ) + + def create_histogram(self, name, unit="", description="") -> Histogram: + """Returns a no-op Histogram.""" + super().create_histogram(name, unit=unit, description=description) + if self._check_instrument_id(name, NoOpHistogram, unit, description): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + Histogram.__name__, + unit, + description, + ) + return NoOpHistogram(name, unit=unit, description=description) + + def create_observable_gauge( + self, name, callbacks=None, unit="", description="" + ) -> ObservableGauge: + """Returns a no-op ObservableGauge.""" + super().create_observable_gauge( + name, callbacks, unit=unit, description=description + ) + if self._check_instrument_id( + name, NoOpObservableGauge, unit, description + ): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + ObservableGauge.__name__, + unit, + description, + ) + return NoOpObservableGauge( + name, + callbacks, + unit=unit, + description=description, + ) + + def create_observable_up_down_counter( + self, name, callbacks=None, unit="", description="" + ) -> ObservableUpDownCounter: + """Returns a no-op ObservableUpDownCounter.""" + super().create_observable_up_down_counter( + name, callbacks, unit=unit, description=description + ) + if self._check_instrument_id( + name, NoOpObservableUpDownCounter, unit, description + ): + _logger.warning( + "An instrument with name %s, type %s, unit %s and " + "description %s has been created already.", + name, + ObservableUpDownCounter.__name__, + unit, + description, + ) + return NoOpObservableUpDownCounter( + name, + callbacks, + unit=unit, + description=description, + ) + + +_METER_PROVIDER_SET_ONCE = Once() +_METER_PROVIDER: Optional[MeterProvider] = None +_PROXY_METER_PROVIDER = _ProxyMeterProvider() + + +def get_meter( + name: str, + version: str = "", + meter_provider: Optional[MeterProvider] = None, +) -> "Meter": + """Returns a `Meter` for use by the given instrumentation library. + + This function is a convenience wrapper for + opentelemetry.metrics.MeterProvider.get_meter. + + If meter_provider is omitted the current configured one is used. + """ + if meter_provider is None: + meter_provider = get_meter_provider() + return meter_provider.get_meter(name, version) + + +def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None: + def set_mp() -> None: + global _METER_PROVIDER # pylint: disable=global-statement + _METER_PROVIDER = meter_provider + + # gives all proxies real instruments off the newly set meter provider + _PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider) + + did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp) + + if log and not did_set: + _logger.warning("Overriding of current MeterProvider is not allowed") + + +def set_meter_provider(meter_provider: MeterProvider) -> None: + """Sets the current global :class:`~.MeterProvider` object. + + This can only be done once, a warning will be logged if any furter attempt + is made. + """ + _set_meter_provider(meter_provider, log=True) + + +def get_meter_provider() -> MeterProvider: + """Gets the current global :class:`~.MeterProvider` object.""" + + if _METER_PROVIDER is None: + if OTEL_PYTHON_METER_PROVIDER not in environ.keys(): + return _PROXY_METER_PROVIDER + + meter_provider: MeterProvider = _load_provider( + OTEL_PYTHON_METER_PROVIDER, "meter_provider" + ) + _set_meter_provider(meter_provider, log=False) + + # _METER_PROVIDER will have been set by one thread + return cast("MeterProvider", _METER_PROVIDER) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py index 30b9230be54..2685775bf8e 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/__init__.py @@ -12,660 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-ancestors +# FIXME Find a way to allow mypy to run here. +# Without this mypy fails with the error described here: +# https://github.com/python/mypy/issues/7182 # type: ignore -""" -The OpenTelemetry metrics API describes the classes used to generate -metrics. -The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in -turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are -used to record measurements. - -This module provides abstract (i.e. unimplemented) classes required for -metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications -to use the API package alone without a supporting implementation. - -To get a meter, you need to provide the package name from which you are -calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter` -with the calling instrumentation name and the version of your package. - -The following code shows how to obtain a meter using the global :class:`.MeterProvider`:: - - from opentelemetry._metrics import get_meter - - meter = get_meter("example-meter") - counter = meter.create_counter("example-counter") - -.. versionadded:: 1.10.0 -""" - - -from abc import ABC, abstractmethod -from logging import getLogger -from os import environ -from threading import Lock -from typing import List, Optional, Set, cast - -from opentelemetry._metrics.instrument import ( - Counter, - Histogram, - NoOpCounter, - NoOpHistogram, - NoOpObservableCounter, - NoOpObservableGauge, - NoOpObservableUpDownCounter, - NoOpUpDownCounter, - ObservableCounter, - ObservableGauge, - ObservableUpDownCounter, - UpDownCounter, - _ProxyCounter, - _ProxyHistogram, - _ProxyInstrument, - _ProxyObservableCounter, - _ProxyObservableGauge, - _ProxyObservableUpDownCounter, - _ProxyUpDownCounter, -) -from opentelemetry.environment_variables import ( - _OTEL_PYTHON_METER_PROVIDER as OTEL_PYTHON_METER_PROVIDER, +from opentelemetry._metrics.___init__ import ( + Meter, + MeterProvider, + NoOpMeter, + NoOpMeterProvider, + get_meter, + get_meter_provider, + set_meter_provider, ) -from opentelemetry.util._once import Once -from opentelemetry.util._providers import _load_provider - -_logger = getLogger(__name__) - - -class MeterProvider(ABC): - """ - MeterProvider is the entry point of the API. It provides access to `Meter` instances. - """ - - @abstractmethod - def get_meter( - self, - name: str, - version: str = None, - schema_url: str = None, - ) -> "Meter": - """Returns a `Meter` for use by the given instrumentation library. - - For any two calls it is undefined whether the same or different - `Meter` instances are returned, even for different library names. - - This function may return different `Meter` types (e.g. a no-op meter - vs. a functional meter). - - Args: - name: The name of the instrumenting module. - ``__name__`` may not be used as this can result in - different meter names if the meters are in different files. - It is better to use a fixed string that can be imported where - needed and used consistently as the name of the meter. - - This should *not* be the name of the module that is - instrumented but the name of the module doing the instrumentation. - E.g., instead of ``"requests"``, use - ``"opentelemetry.instrumentation.requests"``. - - version: Optional. The version string of the - instrumenting library. Usually this should be the same as - ``pkg_resources.get_distribution(instrumenting_library_name).version``. - - schema_url: Optional. Specifies the Schema URL of the emitted telemetry. - """ - - -class NoOpMeterProvider(MeterProvider): - """The default MeterProvider used when no MeterProvider implementation is available.""" - - def get_meter( - self, - name, - version=None, - schema_url=None, - ) -> "Meter": - """Returns a NoOpMeter.""" - super().get_meter(name, version=version, schema_url=schema_url) - return NoOpMeter(name, version=version, schema_url=schema_url) - - -class _ProxyMeterProvider(MeterProvider): - def __init__(self) -> None: - self._lock = Lock() - self._meters: List[_ProxyMeter] = [] - self._real_meter_provider: Optional[MeterProvider] = None - - def get_meter( - self, - name, - version=None, - schema_url=None, - ) -> "Meter": - with self._lock: - if self._real_meter_provider is not None: - return self._real_meter_provider.get_meter( - name, version, schema_url - ) - - meter = _ProxyMeter(name, version=version, schema_url=schema_url) - self._meters.append(meter) - return meter - - def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: - with self._lock: - self._real_meter_provider = meter_provider - for meter in self._meters: - meter.on_set_meter_provider(meter_provider) - - -class Meter(ABC): - """Handles instrument creation. - - This class provides methods for creating instruments which are then - used to produce measurements. - """ - - def __init__(self, name: str, version: str = None, schema_url: str = None): - super().__init__() - self._name = name - self._version = version - self._schema_url = schema_url - self._instrument_ids: Set[str] = set() - self._instrument_ids_lock = Lock() - - @property - def name(self): - """ - The name of the instrumenting module. - """ - return self._name - - @property - def version(self): - """ - The version string of the instrumenting library. - """ - return self._version - - @property - def schema_url(self): - """ - Specifies the Schema URL of the emitted telemetry - """ - return self._schema_url - - def _check_instrument_id( - self, name: str, type_: type, unit: str, description: str - ) -> bool: - """ - Check if an instrument with the same name, type, unit and description - has been registered already. - """ - - result = False - - instrument_id = ",".join( - [name.strip().lower(), type_.__name__, unit, description] - ) - - with self._instrument_ids_lock: - if instrument_id in self._instrument_ids: - result = True - else: - self._instrument_ids.add(instrument_id) - - return result - - @abstractmethod - def create_counter( - self, name: str, unit: str = "", description: str = "" - ) -> Counter: - """Creates a `Counter` instrument - - Args: - name: The name of the instrument to be created - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_up_down_counter( - self, name: str, unit: str = "", description: str = "" - ) -> UpDownCounter: - """Creates an `UpDownCounter` instrument - - Args: - name: The name of the instrument to be created - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_observable_counter( - self, name, callbacks=None, unit="", description="" - ) -> ObservableCounter: - """Creates an `ObservableCounter` instrument - - An observable counter observes a monotonically increasing count by - calling provided callbacks which returns multiple - :class:`~opentelemetry._metrics.observation.Observation`. - - For example, an observable counter could be used to report system CPU - time periodically. Here is a basic implementation:: - - def cpu_time_callback() -> Iterable[Observation]: - observations = [] - with open("/proc/stat") as procstat: - procstat.readline() # skip the first line - for line in procstat: - if not line.startswith("cpu"): break - cpu, *states = line.split() - observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) - observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) - observations.append(Observation(int(states[2]) // 100, {"cpu": cpu, "state": "system"})) - # ... other states - return observations - - meter.create_observable_counter( - "system.cpu.time", - callbacks=[cpu_time_callback], - unit="s", - description="CPU time" - ) - - To reduce memory usage, you can use generator callbacks instead of - building the full list:: - - def cpu_time_callback() -> Iterable[Observation]: - with open("/proc/stat") as procstat: - procstat.readline() # skip the first line - for line in procstat: - if not line.startswith("cpu"): break - cpu, *states = line.split() - yield Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"}) - yield Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}) - # ... other states - - Alternatively, you can pass a sequence of generators directly instead - of a sequence of callbacks, which each should return iterables of - :class:`~opentelemetry._metrics.observation.Observation`:: - - def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]: - while True: - observations = [] - with open("/proc/stat") as procstat: - procstat.readline() # skip the first line - for line in procstat: - if not line.startswith("cpu"): break - cpu, *states = line.split() - if "user" in states_to_include: - observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})) - if "nice" in states_to_include: - observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})) - # ... other states - yield observations - - meter.create_observable_counter( - "system.cpu.time", - callbacks=[cpu_time_callback({"user", "system"})], - unit="s", - description="CPU time" - ) - - Args: - name: The name of the instrument to be created - callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.observation.Observation`. - Alternatively, can be a sequence of generators that each yields - iterables of - :class:`~opentelemetry._metrics.observation.Observation`. - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_histogram(self, name, unit="", description="") -> Histogram: - """Creates a `opentelemetry._metrics.instrument.Histogram` instrument - - Args: - name: The name of the instrument to be created - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_observable_gauge( - self, name, callbacks=None, unit="", description="" - ) -> ObservableGauge: - """Creates an `ObservableGauge` instrument - - Args: - name: The name of the instrument to be created - callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.observation.Observation`. - Alternatively, can be a generator that yields iterables of - :class:`~opentelemetry._metrics.observation.Observation`. - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - @abstractmethod - def create_observable_up_down_counter( - self, name, callbacks=None, unit="", description="" - ) -> ObservableUpDownCounter: - """Creates an `ObservableUpDownCounter` instrument - - Args: - name: The name of the instrument to be created - callbacks: A sequence of callbacks that return an iterable of - :class:`~opentelemetry._metrics.observation.Observation`. - Alternatively, can be a generator that yields iterables of - :class:`~opentelemetry._metrics.observation.Observation`. - unit: The unit for observations this instrument reports. For - example, ``By`` for bytes. UCUM units are recommended. - description: A description for this instrument and what it measures. - """ - - -class _ProxyMeter(Meter): - def __init__( - self, - name, - version=None, - schema_url=None, - ): - super().__init__(name, version=version, schema_url=schema_url) - self._lock = Lock() - self._instruments: List[_ProxyInstrument] = [] - self._real_meter: Optional[Meter] = None - - def on_set_meter_provider(self, meter_provider: MeterProvider) -> None: - """Called when a real meter provider is set on the creating _ProxyMeterProvider - - Creates a real backing meter for this instance and notifies all created - instruments so they can create real backing instruments. - """ - real_meter = meter_provider.get_meter( - self._name, self._version, self._schema_url - ) - - with self._lock: - self._real_meter = real_meter - # notify all proxy instruments of the new meter so they can create - # real instruments to back themselves - for instrument in self._instruments: - instrument.on_meter_set(real_meter) - - def create_counter(self, name, unit="", description="") -> Counter: - with self._lock: - if self._real_meter: - return self._real_meter.create_counter(name, unit, description) - proxy = _ProxyCounter(name, unit, description) - self._instruments.append(proxy) - return proxy - - def create_up_down_counter( - self, name, unit="", description="" - ) -> UpDownCounter: - with self._lock: - if self._real_meter: - return self._real_meter.create_up_down_counter( - name, unit, description - ) - proxy = _ProxyUpDownCounter(name, unit, description) - self._instruments.append(proxy) - return proxy - - def create_observable_counter( - self, name, callbacks=None, unit="", description="" - ) -> ObservableCounter: - with self._lock: - if self._real_meter: - return self._real_meter.create_observable_counter( - name, callbacks, unit, description - ) - proxy = _ProxyObservableCounter( - name, callbacks, unit=unit, description=description - ) - self._instruments.append(proxy) - return proxy - - def create_histogram(self, name, unit="", description="") -> Histogram: - with self._lock: - if self._real_meter: - return self._real_meter.create_histogram( - name, unit, description - ) - proxy = _ProxyHistogram(name, unit, description) - self._instruments.append(proxy) - return proxy - - def create_observable_gauge( - self, name, callbacks=None, unit="", description="" - ) -> ObservableGauge: - with self._lock: - if self._real_meter: - return self._real_meter.create_observable_gauge( - name, callbacks, unit, description - ) - proxy = _ProxyObservableGauge( - name, callbacks, unit=unit, description=description - ) - self._instruments.append(proxy) - return proxy - - def create_observable_up_down_counter( - self, name, callbacks=None, unit="", description="" - ) -> ObservableUpDownCounter: - with self._lock: - if self._real_meter: - return self._real_meter.create_observable_up_down_counter( - name, - callbacks, - unit, - description, - ) - proxy = _ProxyObservableUpDownCounter( - name, callbacks, unit=unit, description=description - ) - self._instruments.append(proxy) - return proxy - - -class NoOpMeter(Meter): - """The default Meter used when no Meter implementation is available. - - All operations are no-op. - """ - - def create_counter(self, name, unit="", description="") -> Counter: - """Returns a no-op Counter.""" - super().create_counter(name, unit=unit, description=description) - if self._check_instrument_id(name, NoOpCounter, unit, description): - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - Counter.__name__, - unit, - description, - ) - return NoOpCounter(name, unit=unit, description=description) - - def create_up_down_counter( - self, name, unit="", description="" - ) -> UpDownCounter: - """Returns a no-op UpDownCounter.""" - super().create_up_down_counter( - name, unit=unit, description=description - ) - if self._check_instrument_id( - name, NoOpUpDownCounter, unit, description - ): - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - UpDownCounter.__name__, - unit, - description, - ) - return NoOpUpDownCounter(name, unit=unit, description=description) - - def create_observable_counter( - self, name, callbacks=None, unit="", description="" - ) -> ObservableCounter: - """Returns a no-op ObservableCounter.""" - super().create_observable_counter( - name, callbacks, unit=unit, description=description - ) - if self._check_instrument_id( - name, NoOpObservableCounter, unit, description - ): - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - ObservableCounter.__name__, - unit, - description, - ) - return NoOpObservableCounter( - name, - callbacks, - unit=unit, - description=description, - ) - - def create_histogram(self, name, unit="", description="") -> Histogram: - """Returns a no-op Histogram.""" - super().create_histogram(name, unit=unit, description=description) - if self._check_instrument_id(name, NoOpHistogram, unit, description): - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - Histogram.__name__, - unit, - description, - ) - return NoOpHistogram(name, unit=unit, description=description) - - def create_observable_gauge( - self, name, callbacks=None, unit="", description="" - ) -> ObservableGauge: - """Returns a no-op ObservableGauge.""" - super().create_observable_gauge( - name, callbacks, unit=unit, description=description - ) - if self._check_instrument_id( - name, NoOpObservableGauge, unit, description - ): - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - ObservableGauge.__name__, - unit, - description, - ) - return NoOpObservableGauge( - name, - callbacks, - unit=unit, - description=description, - ) - - def create_observable_up_down_counter( - self, name, callbacks=None, unit="", description="" - ) -> ObservableUpDownCounter: - """Returns a no-op ObservableUpDownCounter.""" - super().create_observable_up_down_counter( - name, callbacks, unit=unit, description=description - ) - if self._check_instrument_id( - name, NoOpObservableUpDownCounter, unit, description - ): - _logger.warning( - "An instrument with name %s, type %s, unit %s and " - "description %s has been created already.", - name, - ObservableUpDownCounter.__name__, - unit, - description, - ) - return NoOpObservableUpDownCounter( - name, - callbacks, - unit=unit, - description=description, - ) - - -_METER_PROVIDER_SET_ONCE = Once() -_METER_PROVIDER: Optional[MeterProvider] = None -_PROXY_METER_PROVIDER = _ProxyMeterProvider() - - -def get_meter( - name: str, - version: str = "", - meter_provider: Optional[MeterProvider] = None, -) -> "Meter": - """Returns a `Meter` for use by the given instrumentation library. - - This function is a convenience wrapper for - opentelemetry.trace.MeterProvider.get_meter. - - If meter_provider is omitted the current configured one is used. - """ - if meter_provider is None: - meter_provider = get_meter_provider() - return meter_provider.get_meter(name, version) - - -def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None: - def set_mp() -> None: - global _METER_PROVIDER # pylint: disable=global-statement - _METER_PROVIDER = meter_provider - - # gives all proxies real instruments off the newly set meter provider - _PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider) - - did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp) - - if log and not did_set: - _logger.warning("Overriding of current MeterProvider is not allowed") - - -def set_meter_provider(meter_provider: MeterProvider) -> None: - """Sets the current global :class:`~.MeterProvider` object. - - This can only be done once, a warning will be logged if any furter attempt - is made. - """ - _set_meter_provider(meter_provider, log=True) - - -def get_meter_provider() -> MeterProvider: - """Gets the current global :class:`~.MeterProvider` object.""" - - if _METER_PROVIDER is None: - if OTEL_PYTHON_METER_PROVIDER not in environ.keys(): - return _PROXY_METER_PROVIDER - - meter_provider: MeterProvider = _load_provider( - OTEL_PYTHON_METER_PROVIDER, "meter_provider" - ) - _set_meter_provider(meter_provider, log=False) - # _METER_PROVIDER will have been set by one thread - return cast("MeterProvider", _METER_PROVIDER) +__all__ = [ + "MeterProvider", + "NoOpMeterProvider", + "Meter", + "NoOpMeter", + "get_meter", + "set_meter_provider", + "get_meter_provider", +] diff --git a/opentelemetry-api/src/opentelemetry/_metrics/_instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/_instrument.py new file mode 100644 index 00000000000..ec8161a7a55 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/_metrics/_instrument.py @@ -0,0 +1,277 @@ +# 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. + +# pylint: disable=too-many-ancestors + +# type: ignore + +from abc import ABC, abstractmethod +from logging import getLogger +from typing import ( + Callable, + Generator, + Generic, + Iterable, + Optional, + TypeVar, + Union, +) + +# pylint: disable=unused-import; needed for typing and sphinx +from opentelemetry import _metrics as metrics +from opentelemetry._metrics.observation import Observation + +InstrumentT = TypeVar("InstrumentT", bound="Instrument") +CallbackT = Union[ + Callable[[], Iterable[Observation]], + Generator[Iterable[Observation], None, None], +] + + +_logger = getLogger(__name__) + + +class Instrument(ABC): + """Abstract class that serves as base for all instruments.""" + + @abstractmethod + def __init__(self, name, unit="", description=""): + pass + + # FIXME check that the instrument name is valid + # FIXME check that the unit is 63 characters or shorter + # FIXME check that the unit contains only ASCII characters + + +class _ProxyInstrument(ABC, Generic[InstrumentT]): + def __init__(self, name, unit, description) -> None: + self._name = name + self._unit = unit + self._description = description + self._real_instrument: Optional[InstrumentT] = None + + def on_meter_set(self, meter: "metrics.Meter") -> None: + """Called when a real meter is set on the creating _ProxyMeter""" + + # We don't need any locking on proxy instruments because it's OK if some + # measurements get dropped while a real backing instrument is being + # created. + self._real_instrument = self._create_real_instrument(meter) + + @abstractmethod + def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT: + """Create an instance of the real instrument. Implement this.""" + + +class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]): + def __init__(self, name, callbacks, unit, description) -> None: + super().__init__(name, unit, description) + self._callbacks = callbacks + + +class _Synchronous(Instrument): + pass + + +class _Asynchronous(Instrument): + @abstractmethod + def __init__( + self, + name, + callbacks=None, + unit="", + description="", + ): + super().__init__(name, unit=unit, description=description) + + +class _Adding(Instrument): + pass + + +class _Grouping(Instrument): + pass + + +class _Monotonic(_Adding): + pass + + +class _NonMonotonic(_Adding): + pass + + +class Counter(_Monotonic, _Synchronous): + """A Counter is a synchronous `Instrument` which supports non-negative increments.""" + + @abstractmethod + def add(self, amount, attributes=None): + # FIXME check that the amount is non negative + pass + + +class NoOpCounter(Counter): + """No-op implementation of `Counter`.""" + + def __init__(self, name, unit="", description=""): + super().__init__(name, unit=unit, description=description) + + def add(self, amount, attributes=None): + return super().add(amount, attributes=attributes) + + +class _ProxyCounter(_ProxyInstrument[Counter], Counter): + def add(self, amount, attributes=None): + if self._real_instrument: + self._real_instrument.add(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> Counter: + return meter.create_counter(self._name, self._unit, self._description) + + +class UpDownCounter(_NonMonotonic, _Synchronous): + """An UpDownCounter is a synchronous `Instrument` which supports increments and decrements.""" + + @abstractmethod + def add(self, amount, attributes=None): + pass + + +class NoOpUpDownCounter(UpDownCounter): + """No-op implementation of `UpDownCounter`.""" + + def __init__(self, name, unit="", description=""): + super().__init__(name, unit=unit, description=description) + + def add(self, amount, attributes=None): + return super().add(amount, attributes=attributes) + + +class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter): + def add(self, amount, attributes=None): + if self._real_instrument: + self._real_instrument.add(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter: + return meter.create_up_down_counter( + self._name, self._unit, self._description + ) + + +class ObservableCounter(_Monotonic, _Asynchronous): + """An ObservableCounter is an asynchronous `Instrument` which reports monotonically + increasing value(s) when the instrument is being observed. + """ + + +class NoOpObservableCounter(ObservableCounter): + """No-op implementation of `ObservableCounter`.""" + + def __init__(self, name, callbacks=None, unit="", description=""): + super().__init__(name, callbacks, unit=unit, description=description) + + +class _ProxyObservableCounter( + _ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter +): + def _create_real_instrument( + self, meter: "metrics.Meter" + ) -> ObservableCounter: + return meter.create_observable_counter( + self._name, self._callbacks, self._unit, self._description + ) + + +class ObservableUpDownCounter(_NonMonotonic, _Asynchronous): + """An ObservableUpDownCounter is an asynchronous `Instrument` which reports additive value(s) (e.g. + the process heap size - it makes sense to report the heap size from multiple processes and sum them + up, so we get the total heap usage) when the instrument is being observed. + """ + + +class NoOpObservableUpDownCounter(ObservableUpDownCounter): + """No-op implementation of `ObservableUpDownCounter`.""" + + def __init__(self, name, callbacks=None, unit="", description=""): + super().__init__(name, callbacks, unit=unit, description=description) + + +class _ProxyObservableUpDownCounter( + _ProxyAsynchronousInstrument[ObservableUpDownCounter], + ObservableUpDownCounter, +): + def _create_real_instrument( + self, meter: "metrics.Meter" + ) -> ObservableUpDownCounter: + return meter.create_observable_up_down_counter( + self._name, self._callbacks, self._unit, self._description + ) + + +class Histogram(_Grouping, _Synchronous): + """Histogram is a synchronous `Instrument` which can be used to report arbitrary values + that are likely to be statistically meaningful. It is intended for statistics such as + histograms, summaries, and percentile. + """ + + @abstractmethod + def record(self, amount, attributes=None): + pass + + +class NoOpHistogram(Histogram): + """No-op implementation of `Histogram`.""" + + def __init__(self, name, unit="", description=""): + super().__init__(name, unit=unit, description=description) + + def record(self, amount, attributes=None): + return super().record(amount, attributes=attributes) + + +class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram): + def record(self, amount, attributes=None): + if self._real_instrument: + self._real_instrument.record(amount, attributes) + + def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram: + return meter.create_histogram( + self._name, self._unit, self._description + ) + + +class ObservableGauge(_Grouping, _Asynchronous): + """Asynchronous Gauge is an asynchronous `Instrument` which reports non-additive value(s) (e.g. + the room temperature - it makes no sense to report the temperature value from multiple rooms + and sum them up) when the instrument is being observed. + """ + + +class NoOpObservableGauge(ObservableGauge): + """No-op implementation of `ObservableGauge`.""" + + def __init__(self, name, callbacks=None, unit="", description=""): + super().__init__(name, callbacks, unit=unit, description=description) + + +class _ProxyObservableGauge( + _ProxyAsynchronousInstrument[ObservableGauge], + ObservableGauge, +): + def _create_real_instrument( + self, meter: "metrics.Meter" + ) -> ObservableGauge: + return meter.create_observable_gauge( + self._name, self._callbacks, self._unit, self._description + ) diff --git a/opentelemetry-api/src/opentelemetry/_metrics/_observation.py b/opentelemetry-api/src/opentelemetry/_metrics/_observation.py new file mode 100644 index 00000000000..7aa24e3342d --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/_metrics/_observation.py @@ -0,0 +1,52 @@ +# 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 typing import Union + +from opentelemetry.util.types import Attributes + + +class Observation: + """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: Union[int, float], attributes: Attributes = None + ) -> None: + self._value = value + self._attributes = attributes + + @property + def value(self) -> Union[float, int]: + return self._value + + @property + def attributes(self) -> Attributes: + return self._attributes + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, Observation) + and self.value == other.value + and self.attributes == other.attributes + ) + + def __repr__(self) -> str: + return f"Observation(value={self.value}, attributes={self.attributes})" diff --git a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py index d79b0d9aa60..f7223026f4e 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/instrument.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/instrument.py @@ -12,266 +12,44 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-ancestors - +# FIXME Find a way to allow mypy to run here. +# Without this mypy fails with the error described here: +# https://github.com/python/mypy/issues/7182 # type: ignore -from abc import ABC, abstractmethod -from logging import getLogger -from typing import ( - Callable, - Generator, - Generic, - Iterable, - Optional, - TypeVar, - Union, -) - -# pylint: disable=unused-import; needed for typing and sphinx -from opentelemetry import _metrics as metrics -from opentelemetry._metrics.observation import Observation - -InstrumentT = TypeVar("InstrumentT", bound="Instrument") -CallbackT = Union[ - Callable[[], Iterable[Observation]], - Generator[Iterable[Observation], None, None], -] - - -_logger = getLogger(__name__) - - -class Instrument(ABC): - """Abstract class that serves as base for all instruments.""" - - @abstractmethod - def __init__(self, name, unit="", description=""): - pass - - # FIXME check that the instrument name is valid - # FIXME check that the unit is 63 characters or shorter - # FIXME check that the unit contains only ASCII characters - - -class _ProxyInstrument(ABC, Generic[InstrumentT]): - def __init__(self, name, unit, description) -> None: - self._name = name - self._unit = unit - self._description = description - self._real_instrument: Optional[InstrumentT] = None - - def on_meter_set(self, meter: "metrics.Meter") -> None: - """Called when a real meter is set on the creating _ProxyMeter""" - - # We don't need any locking on proxy instruments because it's OK if some - # measurements get dropped while a real backing instrument is being - # created. - self._real_instrument = self._create_real_instrument(meter) - - @abstractmethod - def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT: - """Create an instance of the real instrument. Implement this.""" - - -class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]): - def __init__(self, name, callbacks, unit, description) -> None: - super().__init__(name, unit, description) - self._callbacks = callbacks - - -class Synchronous(Instrument): - pass - - -class Asynchronous(Instrument): - @abstractmethod - def __init__( - self, - name, - callbacks=None, - unit="", - description="", - ): - super().__init__(name, unit=unit, description=description) - - -class _Adding(Instrument): - pass - - -class _Grouping(Instrument): - pass - - -class _Monotonic(_Adding): - pass - - -class _NonMonotonic(_Adding): - pass - - -class Counter(_Monotonic, Synchronous): - """A Counter is a synchronous `Instrument` which supports non-negative increments.""" - - @abstractmethod - def add(self, amount, attributes=None): - # FIXME check that the amount is non negative - pass - - -class NoOpCounter(Counter): - """No-op implementation of `Counter`.""" - - def __init__(self, name, unit="", description=""): - super().__init__(name, unit=unit, description=description) - - def add(self, amount, attributes=None): - return super().add(amount, attributes=attributes) - - -class _ProxyCounter(_ProxyInstrument[Counter], Counter): - def add(self, amount, attributes=None): - if self._real_instrument: - self._real_instrument.add(amount, attributes) - - def _create_real_instrument(self, meter: "metrics.Meter") -> Counter: - return meter.create_counter(self._name, self._unit, self._description) - - -class UpDownCounter(_NonMonotonic, Synchronous): - """An UpDownCounter is a synchronous `Instrument` which supports increments and decrements.""" - @abstractmethod - def add(self, amount, attributes=None): - pass - - -class NoOpUpDownCounter(UpDownCounter): - """No-op implementation of `UpDownCounter`.""" - - def __init__(self, name, unit="", description=""): - super().__init__(name, unit=unit, description=description) - - def add(self, amount, attributes=None): - return super().add(amount, attributes=attributes) - - -class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter): - def add(self, amount, attributes=None): - if self._real_instrument: - self._real_instrument.add(amount, attributes) - - def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter: - return meter.create_up_down_counter( - self._name, self._unit, self._description - ) - - -class ObservableCounter(_Monotonic, Asynchronous): - """An ObservableCounter is an asynchronous `Instrument` which reports monotonically - increasing value(s) when the instrument is being observed. - """ - - -class NoOpObservableCounter(ObservableCounter): - """No-op implementation of `ObservableCounter`.""" - - def __init__(self, name, callbacks=None, unit="", description=""): - super().__init__(name, callbacks, unit=unit, description=description) - - -class _ProxyObservableCounter( - _ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter -): - def _create_real_instrument( - self, meter: "metrics.Meter" - ) -> ObservableCounter: - return meter.create_observable_counter( - self._name, self._callbacks, self._unit, self._description - ) - - -class ObservableUpDownCounter(_NonMonotonic, Asynchronous): - """An ObservableUpDownCounter is an asynchronous `Instrument` which reports additive value(s) (e.g. - the process heap size - it makes sense to report the heap size from multiple processes and sum them - up, so we get the total heap usage) when the instrument is being observed. - """ - - -class NoOpObservableUpDownCounter(ObservableUpDownCounter): - """No-op implementation of `ObservableUpDownCounter`.""" - - def __init__(self, name, callbacks=None, unit="", description=""): - super().__init__(name, callbacks, unit=unit, description=description) - - -class _ProxyObservableUpDownCounter( - _ProxyAsynchronousInstrument[ObservableUpDownCounter], +from opentelemetry._metrics._instrument import ( + CallbackT, + Counter, + Histogram, + Instrument, + InstrumentT, + NoOpCounter, + NoOpHistogram, + NoOpObservableCounter, + NoOpObservableGauge, + NoOpObservableUpDownCounter, + NoOpUpDownCounter, + ObservableCounter, + ObservableGauge, ObservableUpDownCounter, -): - def _create_real_instrument( - self, meter: "metrics.Meter" - ) -> ObservableUpDownCounter: - return meter.create_observable_up_down_counter( - self._name, self._callbacks, self._unit, self._description - ) - - -class Histogram(_Grouping, Synchronous): - """Histogram is a synchronous `Instrument` which can be used to report arbitrary values - that are likely to be statistically meaningful. It is intended for statistics such as - histograms, summaries, and percentile. - """ - - @abstractmethod - def record(self, amount, attributes=None): - pass - - -class NoOpHistogram(Histogram): - """No-op implementation of `Histogram`.""" - - def __init__(self, name, unit="", description=""): - super().__init__(name, unit=unit, description=description) - - def record(self, amount, attributes=None): - return super().record(amount, attributes=attributes) - - -class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram): - def record(self, amount, attributes=None): - if self._real_instrument: - self._real_instrument.record(amount, attributes) - - def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram: - return meter.create_histogram( - self._name, self._unit, self._description - ) - - -class ObservableGauge(_Grouping, Asynchronous): - """Asynchronous Gauge is an asynchronous `Instrument` which reports non-additive value(s) (e.g. - the room temperature - it makes no sense to report the temperature value from multiple rooms - and sum them up) when the instrument is being observed. - """ - - -class NoOpObservableGauge(ObservableGauge): - """No-op implementation of `ObservableGauge`.""" - - def __init__(self, name, callbacks=None, unit="", description=""): - super().__init__(name, callbacks, unit=unit, description=description) - + UpDownCounter, +) -class _ProxyObservableGauge( - _ProxyAsynchronousInstrument[ObservableGauge], - ObservableGauge, -): - def _create_real_instrument( - self, meter: "metrics.Meter" - ) -> ObservableGauge: - return meter.create_observable_gauge( - self._name, self._callbacks, self._unit, self._description - ) +__all__ = [ + "Instrument", + "Counter", + "NoOpCounter", + "UpDownCounter", + "NoOpUpDownCounter", + "ObservableCounter", + "NoOpObservableCounter", + "ObservableUpDownCounter", + "NoOpObservableUpDownCounter", + "Histogram", + "NoOpHistogram", + "ObservableGauge", + "NoOpObservableGauge", + "InstrumentT", + "CallbackT", +] diff --git a/opentelemetry-api/src/opentelemetry/_metrics/observation.py b/opentelemetry-api/src/opentelemetry/_metrics/observation.py index 7aa24e3342d..96971236bd0 100644 --- a/opentelemetry-api/src/opentelemetry/_metrics/observation.py +++ b/opentelemetry-api/src/opentelemetry/_metrics/observation.py @@ -12,41 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union -from opentelemetry.util.types import Attributes +from opentelemetry._metrics._observation import Observation - -class Observation: - """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: Union[int, float], attributes: Attributes = None - ) -> None: - self._value = value - self._attributes = attributes - - @property - def value(self) -> Union[float, int]: - return self._value - - @property - def attributes(self) -> Attributes: - return self._attributes - - def __eq__(self, other: object) -> bool: - return ( - isinstance(other, Observation) - and self.value == other.value - and self.attributes == other.attributes - ) - - def __repr__(self) -> str: - return f"Observation(value={self.value}, attributes={self.attributes})" +__all__ = ["Observation"] diff --git a/opentelemetry-api/tests/metrics/test_meter_provider.py b/opentelemetry-api/tests/metrics/test_meter_provider.py index 9efcfeb33a9..cf4bf970ec0 100644 --- a/opentelemetry-api/tests/metrics/test_meter_provider.py +++ b/opentelemetry-api/tests/metrics/test_meter_provider.py @@ -18,8 +18,8 @@ from pytest import fixture -from opentelemetry import _metrics as metrics -from opentelemetry._metrics import ( +from opentelemetry._metrics import ___init__ as metrics +from opentelemetry._metrics.___init__ import ( NoOpMeter, NoOpMeterProvider, _ProxyMeter, @@ -27,7 +27,7 @@ get_meter_provider, set_meter_provider, ) -from opentelemetry._metrics.instrument import ( +from opentelemetry._metrics._instrument import ( _ProxyCounter, _ProxyHistogram, _ProxyObservableCounter, @@ -70,7 +70,7 @@ def test_set_meter_provider(reset_meter_provider): def test_set_meter_provider_calls_proxy_provider(reset_meter_provider): with patch( - "opentelemetry._metrics._PROXY_METER_PROVIDER" + "opentelemetry._metrics.___init__._PROXY_METER_PROVIDER" ) as mock_proxy_mp: mock_real_mp = Mock() set_meter_provider(mock_real_mp) @@ -94,9 +94,9 @@ def test_get_meter_provider(reset_meter_provider): "os.environ", {OTEL_PYTHON_METER_PROVIDER: "test_meter_provider"} ): - with patch("opentelemetry._metrics._load_provider", Mock()): + with patch("opentelemetry._metrics.___init__._load_provider", Mock()): with patch( - "opentelemetry._metrics.cast", + "opentelemetry._metrics.___init__.cast", Mock(**{"return_value": "test_meter_provider"}), ): assert get_meter_provider() == "test_meter_provider" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py index 29e4972d803..d33c6195608 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py @@ -223,7 +223,7 @@ def create_observable_up_down_counter( class MeterProvider(APIMeterProvider): - r"""See `opentelemetry._metrics.MeterProvider`. + r"""See `opentelemetry._metrics.___init__.MeterProvider`. Args: metric_readers: Register metric readers to collect metrics from the SDK on demand. Each diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py index ec6497e9c94..2374f744861 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/aggregation.py @@ -25,17 +25,17 @@ from threading import Lock from typing import Generic, List, Optional, Sequence, TypeVar -from opentelemetry._metrics.instrument import ( - Asynchronous, +from opentelemetry._metrics._instrument import ( Counter, Histogram, Instrument, ObservableCounter, ObservableGauge, ObservableUpDownCounter, - Synchronous, UpDownCounter, + _Asynchronous, _Monotonic, + _Synchronous, ) from opentelemetry.sdk._metrics.measurement import Measurement from opentelemetry.sdk._metrics.point import AggregationTemporality, Gauge @@ -82,16 +82,16 @@ class DefaultAggregation(_AggregationFactory): This aggregation will create an actual aggregation depending on the instrument type, as specified next: - ============================================= ==================================== - Instrument Aggregation - ============================================= ==================================== - `Counter` `SumAggregation` - `UpDownCounter` `SumAggregation` - `ObservableCounter` `SumAggregation` - `ObservableUpDownCounter` `SumAggregation` - `opentelemetry._metrics.instrument.Histogram` `ExplicitBucketHistogramAggregation` - `ObservableGauge` `LastValueAggregation` - ============================================= ==================================== + ============================================== ==================================== + Instrument Aggregation + ============================================== ==================================== + `Counter` `SumAggregation` + `UpDownCounter` `SumAggregation` + `ObservableCounter` `SumAggregation` + `ObservableUpDownCounter` `SumAggregation` + `opentelemetry._metrics._instrument.Histogram` `ExplicitBucketHistogramAggregation` + `ObservableGauge` `LastValueAggregation` + ============================================== ==================================== """ def _create_aggregation(self, instrument: Instrument) -> _Aggregation: @@ -449,9 +449,9 @@ class SumAggregation(_AggregationFactory): def _create_aggregation(self, instrument: Instrument) -> _Aggregation: temporality = AggregationTemporality.UNSPECIFIED - if isinstance(instrument, Synchronous): + if isinstance(instrument, _Synchronous): temporality = AggregationTemporality.DELTA - elif isinstance(instrument, Asynchronous): + elif isinstance(instrument, _Asynchronous): temporality = AggregationTemporality.CUMULATIVE return _SumAggregation( diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py index f5e4ddaea36..1f510fa434e 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py @@ -14,8 +14,8 @@ import unittest -from opentelemetry import _metrics as metrics_api from opentelemetry import trace as trace_api +from opentelemetry._metrics import ___init__ as metrics_api from opentelemetry.util._once import Once