structure saas with tools

This commit is contained in:
Davidson Gomes
2025-04-25 15:30:54 -03:00
commit 1aef473937
16434 changed files with 6584257 additions and 0 deletions

View File

@@ -0,0 +1,889 @@
# 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
"""
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
"""
import warnings
from abc import ABC, abstractmethod
from dataclasses import dataclass
from logging import getLogger
from os import environ
from threading import Lock
from typing import Dict, List, Optional, Sequence, Union, cast
from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER
from opentelemetry.metrics._internal.instrument import (
CallbackT,
Counter,
Gauge,
Histogram,
NoOpCounter,
NoOpGauge,
NoOpHistogram,
NoOpObservableCounter,
NoOpObservableGauge,
NoOpObservableUpDownCounter,
NoOpUpDownCounter,
ObservableCounter,
ObservableGauge,
ObservableUpDownCounter,
UpDownCounter,
_MetricsHistogramAdvisory,
_ProxyCounter,
_ProxyGauge,
_ProxyHistogram,
_ProxyObservableCounter,
_ProxyObservableGauge,
_ProxyObservableUpDownCounter,
_ProxyUpDownCounter,
)
from opentelemetry.util._once import Once
from opentelemetry.util._providers import _load_provider
from opentelemetry.util.types import (
Attributes,
)
_logger = getLogger(__name__)
# pylint: disable=invalid-name
_ProxyInstrumentT = Union[
_ProxyCounter,
_ProxyHistogram,
_ProxyGauge,
_ProxyObservableCounter,
_ProxyObservableGauge,
_ProxyObservableUpDownCounter,
_ProxyUpDownCounter,
]
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: Optional[str] = None,
schema_url: Optional[str] = None,
attributes: Optional[Attributes] = 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
``importlib.metadata.version(instrumenting_library_name)``.
schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
attributes: Optional. Attributes that are associated with the emitted telemetry.
"""
class NoOpMeterProvider(MeterProvider):
"""The default MeterProvider used when no MeterProvider implementation is available."""
def get_meter(
self,
name: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
attributes: Optional[Attributes] = None,
) -> "Meter":
"""Returns a NoOpMeter."""
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: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
attributes: Optional[Attributes] = 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)
@dataclass
class _InstrumentRegistrationStatus:
instrument_id: str
already_registered: bool
conflict: bool
current_advisory: Optional[_MetricsHistogramAdvisory]
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: Optional[str] = None,
schema_url: Optional[str] = None,
) -> None:
super().__init__()
self._name = name
self._version = version
self._schema_url = schema_url
self._instrument_ids: Dict[
str, Optional[_MetricsHistogramAdvisory]
] = {}
self._instrument_ids_lock = Lock()
@property
def name(self) -> str:
"""
The name of the instrumenting module.
"""
return self._name
@property
def version(self) -> Optional[str]:
"""
The version string of the instrumenting library.
"""
return self._version
@property
def schema_url(self) -> Optional[str]:
"""
Specifies the Schema URL of the emitted telemetry
"""
return self._schema_url
def _register_instrument(
self,
name: str,
type_: type,
unit: str,
description: str,
advisory: Optional[_MetricsHistogramAdvisory] = None,
) -> _InstrumentRegistrationStatus:
"""
Register an instrument with the name, type, unit and description as
identifying keys and the advisory as value.
Returns a tuple. The first value is the instrument id.
The second value is an `_InstrumentRegistrationStatus` where
`already_registered` is `True` if the instrument has been registered
already.
If `conflict` is set to True the `current_advisory` attribute contains
the registered instrument advisory.
"""
instrument_id = ",".join(
[name.strip().lower(), type_.__name__, unit, description]
)
already_registered = False
conflict = False
current_advisory = None
with self._instrument_ids_lock:
# we are not using get because None is a valid value
already_registered = instrument_id in self._instrument_ids
if already_registered:
current_advisory = self._instrument_ids[instrument_id]
conflict = current_advisory != advisory
else:
self._instrument_ids[instrument_id] = advisory
return _InstrumentRegistrationStatus(
instrument_id=instrument_id,
already_registered=already_registered,
conflict=conflict,
current_advisory=current_advisory,
)
@staticmethod
def _log_instrument_registration_conflict(
name: str,
instrumentation_type: str,
unit: str,
description: str,
status: _InstrumentRegistrationStatus,
) -> None:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already with a "
"different advisory value %s and will be used instead.",
name,
instrumentation_type,
unit,
description,
status.current_advisory,
)
@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: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableCounter:
"""Creates an `ObservableCounter` instrument
An observable counter observes a monotonically increasing count by calling provided
callbacks which accept a :class:`~opentelemetry.metrics.CallbackOptions` and return
multiple :class:`~opentelemetry.metrics.Observation`.
For example, an observable counter could be used to report system CPU
time periodically. Here is a basic implementation::
def cpu_time_callback(options: CallbackOptions) -> 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(options: CallbackOptions) -> 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`::
def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]:
# accept options sent in from OpenTelemetry
options = yield
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 the observations and receive the options for next iteration
options = yield observations
meter.create_observable_counter(
"system.cpu.time",
callbacks=[cpu_time_callback({"user", "system"})],
unit="s",
description="CPU time"
)
The :class:`~opentelemetry.metrics.CallbackOptions` contain a timeout which the
callback should respect. For example if the callback does asynchronous work, like
making HTTP requests, it should respect the timeout::
def scrape_http_callback(options: CallbackOptions) -> Iterable[Observation]:
r = requests.get('http://scrapethis.com', timeout=options.timeout_millis / 10**3)
for value in r.json():
yield Observation(value)
Args:
name: The name of the instrument to be created
callbacks: A sequence of callbacks that return an iterable of
:class:`~opentelemetry.metrics.Observation`. Alternatively, can be a sequence of generators that each
yields iterables of :class:`~opentelemetry.metrics.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: str,
unit: str = "",
description: str = "",
*,
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
) -> Histogram:
"""Creates a :class:`~opentelemetry.metrics.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.
"""
def create_gauge( # type: ignore # pylint: disable=no-self-use
self,
name: str,
unit: str = "",
description: str = "",
) -> Gauge: # pyright: ignore[reportReturnType]
"""Creates a ``Gauge`` 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.
"""
warnings.warn("create_gauge() is not implemented and will be a no-op")
@abstractmethod
def create_observable_gauge(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> 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`. Alternatively, can be a generator that yields iterables
of :class:`~opentelemetry.metrics.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: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> 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`. Alternatively, can be a generator that yields iterables
of :class:`~opentelemetry.metrics.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: str,
version: Optional[str] = None,
schema_url: Optional[str] = None,
) -> None:
super().__init__(name, version=version, schema_url=schema_url)
self._lock = Lock()
self._instruments: List[_ProxyInstrumentT] = []
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: str,
unit: str = "",
description: str = "",
) -> 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: str,
unit: str = "",
description: str = "",
) -> 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: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> 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: str,
unit: str = "",
description: str = "",
*,
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
) -> Histogram:
with self._lock:
if self._real_meter:
return self._real_meter.create_histogram(
name,
unit,
description,
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
)
proxy = _ProxyHistogram(
name, unit, description, explicit_bucket_boundaries_advisory
)
self._instruments.append(proxy)
return proxy
def create_gauge(
self,
name: str,
unit: str = "",
description: str = "",
) -> Gauge:
with self._lock:
if self._real_meter:
return self._real_meter.create_gauge(name, unit, description)
proxy = _ProxyGauge(name, unit, description)
self._instruments.append(proxy)
return proxy
def create_observable_gauge(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> 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: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> 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: str,
unit: str = "",
description: str = "",
) -> Counter:
"""Returns a no-op Counter."""
status = self._register_instrument(
name, NoOpCounter, unit, description
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
Counter.__name__,
unit,
description,
status,
)
return NoOpCounter(name, unit=unit, description=description)
def create_gauge(
self,
name: str,
unit: str = "",
description: str = "",
) -> Gauge:
"""Returns a no-op Gauge."""
status = self._register_instrument(name, NoOpGauge, unit, description)
if status.conflict:
self._log_instrument_registration_conflict(
name,
Gauge.__name__,
unit,
description,
status,
)
return NoOpGauge(name, unit=unit, description=description)
def create_up_down_counter(
self,
name: str,
unit: str = "",
description: str = "",
) -> UpDownCounter:
"""Returns a no-op UpDownCounter."""
status = self._register_instrument(
name, NoOpUpDownCounter, unit, description
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
UpDownCounter.__name__,
unit,
description,
status,
)
return NoOpUpDownCounter(name, unit=unit, description=description)
def create_observable_counter(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableCounter:
"""Returns a no-op ObservableCounter."""
status = self._register_instrument(
name, NoOpObservableCounter, unit, description
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
ObservableCounter.__name__,
unit,
description,
status,
)
return NoOpObservableCounter(
name,
callbacks,
unit=unit,
description=description,
)
def create_histogram(
self,
name: str,
unit: str = "",
description: str = "",
*,
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
) -> Histogram:
"""Returns a no-op Histogram."""
status = self._register_instrument(
name,
NoOpHistogram,
unit,
description,
_MetricsHistogramAdvisory(
explicit_bucket_boundaries=explicit_bucket_boundaries_advisory
),
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
Histogram.__name__,
unit,
description,
status,
)
return NoOpHistogram(
name,
unit=unit,
description=description,
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
)
def create_observable_gauge(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableGauge:
"""Returns a no-op ObservableGauge."""
status = self._register_instrument(
name, NoOpObservableGauge, unit, description
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
ObservableGauge.__name__,
unit,
description,
status,
)
return NoOpObservableGauge(
name,
callbacks,
unit=unit,
description=description,
)
def create_observable_up_down_counter(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> ObservableUpDownCounter:
"""Returns a no-op ObservableUpDownCounter."""
status = self._register_instrument(
name, NoOpObservableUpDownCounter, unit, description
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
ObservableUpDownCounter.__name__,
unit,
description,
status,
)
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,
schema_url: Optional[str] = None,
attributes: Optional[Attributes] = 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, schema_url, attributes)
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 further 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:
return _PROXY_METER_PROVIDER
meter_provider: MeterProvider = _load_provider( # type: ignore
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)

View File

@@ -0,0 +1,530 @@
# 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
from abc import ABC, abstractmethod
from dataclasses import dataclass
from logging import getLogger
from re import compile as re_compile
from typing import (
Callable,
Dict,
Generator,
Generic,
Iterable,
Optional,
Sequence,
TypeVar,
Union,
)
# pylint: disable=unused-import; needed for typing and sphinx
from opentelemetry import metrics
from opentelemetry.context import Context
from opentelemetry.metrics._internal.observation import Observation
from opentelemetry.util.types import (
Attributes,
)
_logger = getLogger(__name__)
_name_regex = re_compile(r"[a-zA-Z][-_./a-zA-Z0-9]{0,254}")
_unit_regex = re_compile(r"[\x00-\x7F]{0,63}")
@dataclass(frozen=True)
class _MetricsHistogramAdvisory:
explicit_bucket_boundaries: Optional[Sequence[float]] = None
@dataclass(frozen=True)
class CallbackOptions:
"""Options for the callback
Args:
timeout_millis: Timeout for the callback's execution. If the callback does asynchronous
work (e.g. HTTP requests), it should respect this timeout.
"""
timeout_millis: float = 10_000
InstrumentT = TypeVar("InstrumentT", bound="Instrument")
# pylint: disable=invalid-name
CallbackT = Union[
Callable[[CallbackOptions], Iterable[Observation]],
Generator[Iterable[Observation], CallbackOptions, None],
]
class Instrument(ABC):
"""Abstract class that serves as base for all instruments."""
@abstractmethod
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
pass
@staticmethod
def _check_name_unit_description(
name: str, unit: str, description: str
) -> Dict[str, Optional[str]]:
"""
Checks the following instrument name, unit and description for
compliance with the spec.
Returns a dict with keys "name", "unit" and "description", the
corresponding values will be the checked strings or `None` if the value
is invalid. If valid, the checked strings should be used instead of the
original values.
"""
result: Dict[str, Optional[str]] = {}
if _name_regex.fullmatch(name) is not None:
result["name"] = name
else:
result["name"] = None
if unit is None:
unit = ""
if _unit_regex.fullmatch(unit) is not None:
result["unit"] = unit
else:
result["unit"] = None
if description is None:
result["description"] = ""
else:
result["description"] = description
return result
class _ProxyInstrument(ABC, Generic[InstrumentT]):
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> 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: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit, description)
self._callbacks = callbacks
class Synchronous(Instrument):
"""Base class for all synchronous instruments"""
class Asynchronous(Instrument):
"""Base class for all asynchronous instruments"""
@abstractmethod
def __init__(
self,
name: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
class Counter(Synchronous):
"""A Counter is a synchronous `Instrument` which supports non-negative increments."""
@abstractmethod
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
pass
class NoOpCounter(Counter):
"""No-op implementation of `Counter`."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
return super().add(amount, attributes=attributes, context=context)
class _ProxyCounter(_ProxyInstrument[Counter], Counter):
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
if self._real_instrument:
self._real_instrument.add(amount, attributes, context)
def _create_real_instrument(self, meter: "metrics.Meter") -> Counter:
return meter.create_counter(
self._name,
self._unit,
self._description,
)
class UpDownCounter(Synchronous):
"""An UpDownCounter is a synchronous `Instrument` which supports increments and decrements."""
@abstractmethod
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
pass
class NoOpUpDownCounter(UpDownCounter):
"""No-op implementation of `UpDownCounter`."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
return super().add(amount, attributes=attributes, context=context)
class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter):
def add(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
if self._real_instrument:
self._real_instrument.add(amount, attributes, context)
def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter:
return meter.create_up_down_counter(
self._name,
self._unit,
self._description,
)
class ObservableCounter(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: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
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(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: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
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(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 __init__(
self,
name: str,
unit: str = "",
description: str = "",
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
) -> None:
pass
@abstractmethod
def record(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
pass
class NoOpHistogram(Histogram):
"""No-op implementation of `Histogram`."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
) -> None:
super().__init__(
name,
unit=unit,
description=description,
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
)
def record(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
return super().record(amount, attributes=attributes, context=context)
class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram):
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
) -> None:
super().__init__(name, unit=unit, description=description)
self._explicit_bucket_boundaries_advisory = (
explicit_bucket_boundaries_advisory
)
def record(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
if self._real_instrument:
self._real_instrument.record(amount, attributes, context)
def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram:
return meter.create_histogram(
self._name,
self._unit,
self._description,
explicit_bucket_boundaries_advisory=self._explicit_bucket_boundaries_advisory,
)
class ObservableGauge(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: str,
callbacks: Optional[Sequence[CallbackT]] = None,
unit: str = "",
description: str = "",
) -> None:
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,
)
class Gauge(Synchronous):
"""A Gauge is a synchronous `Instrument` which can be used to record non-additive values as they occur."""
@abstractmethod
def set(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
pass
class NoOpGauge(Gauge):
"""No-op implementation of ``Gauge``."""
def __init__(
self,
name: str,
unit: str = "",
description: str = "",
) -> None:
super().__init__(name, unit=unit, description=description)
def set(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
return super().set(amount, attributes=attributes, context=context)
class _ProxyGauge(
_ProxyInstrument[Gauge],
Gauge,
):
def set(
self,
amount: Union[int, float],
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
if self._real_instrument:
self._real_instrument.set(amount, attributes, context)
def _create_real_instrument(self, meter: "metrics.Meter") -> Gauge:
return meter.create_gauge(
self._name,
self._unit,
self._description,
)

View File

@@ -0,0 +1,63 @@
# 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 Optional, Union
from opentelemetry.context import Context
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
context: The measurement's context
"""
def __init__(
self,
value: Union[int, float],
attributes: Attributes = None,
context: Optional[Context] = None,
) -> None:
self._value = value
self._attributes = attributes
self._context = context
@property
def value(self) -> Union[float, int]:
return self._value
@property
def attributes(self) -> Attributes:
return self._attributes
@property
def context(self) -> Optional[Context]:
return self._context
def __eq__(self, other: object) -> bool:
return (
isinstance(other, Observation)
and self.value == other.value
and self.attributes == other.attributes
and self.context == other.context
)
def __repr__(self) -> str:
return f"Observation(value={self.value}, attributes={self.attributes}, context={self.context})"