# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 # pylint: disable=too-many-ancestors from abc import ABC, abstractmethod from collections.abc import Callable, Generator, Iterable, Sequence from dataclasses import dataclass from logging import getLogger from re import compile as re_compile from typing import ( Generic, TypeVar, ) # 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: Sequence[float] | None = 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 = ( 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, str | None]: """ 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, str | None] = {} 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: InstrumentT | None = 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: Sequence[CallbackT] | None = 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: Sequence[CallbackT] | None = 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: int | float, attributes: Attributes | None = None, context: Context | None = None, ) -> None: """Records an increment to the counter. Args: amount: The amount to increment the counter by. Must be non-negative. attributes: Optional set of attributes to associate with the measurement. context: Optional context to associate with the measurement. If not provided, the current context is used. """ 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: int | float, attributes: Attributes | None = None, context: Context | None = None, ) -> None: return super().add(amount, attributes=attributes, context=context) class _ProxyCounter(_ProxyInstrument[Counter], Counter): def add( self, amount: int | float, attributes: Attributes | None = None, context: Context | None = 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: int | float, attributes: Attributes | None = None, context: Context | None = None, ) -> None: """Records an increment or decrement to the counter. Unlike `Counter`, the ``amount`` may be negative, allowing the instrument to track values that go up and down (e.g. number of active requests, queue depth). Args: amount: The amount to add to the counter. May be positive or negative. attributes: Optional set of attributes to associate with the measurement. context: Optional context to associate with the measurement. If not provided, the current context is used. """ 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: int | float, attributes: Attributes | None = None, context: Context | None = None, ) -> None: return super().add(amount, attributes=attributes, context=context) class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter): def add( self, amount: int | float, attributes: Attributes | None = None, context: Context | None = 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: Sequence[CallbackT] | None = 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: Sequence[CallbackT] | None = 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: Sequence[float] | None = None, ) -> None: pass @abstractmethod def record( self, amount: int | float, attributes: Attributes | None = None, context: Context | None = None, ) -> None: """Records a measurement. Used to report measurements that are likely to be statistically meaningful, such as request durations, payload sizes, or any value for which a distribution (e.g. percentiles) is useful. Args: amount: The measurement to record. Should be non-negative in most cases; negative values are only meaningful when the histogram is used to track signed deltas. attributes: Optional set of attributes to associate with the measurement. context: Optional context to associate with the measurement. If not provided, the current context is used. """ class NoOpHistogram(Histogram): """No-op implementation of `Histogram`.""" def __init__( self, name: str, unit: str = "", description: str = "", explicit_bucket_boundaries_advisory: Sequence[float] | None = None, ) -> None: super().__init__( name, unit=unit, description=description, explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory, ) def record( self, amount: int | float, attributes: Attributes | None = None, context: Context | None = 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: Sequence[float] | None = None, ) -> None: super().__init__(name, unit=unit, description=description) self._explicit_bucket_boundaries_advisory = ( explicit_bucket_boundaries_advisory ) def record( self, amount: int | float, attributes: Attributes | None = None, context: Context | None = 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: Sequence[CallbackT] | None = 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: int | float, attributes: Attributes | None = None, context: Context | None = None, ) -> None: """Records the current value of the gauge. The gauge reports the last recorded value when observed. It is intended for non-additive measurements where only the current value matters (e.g. CPU utilisation percentage, room temperature). Args: amount: The current value to record. attributes: Optional set of attributes to associate with the measurement. context: Optional context to associate with the measurement. If not provided, the current context is used. """ 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: int | float, attributes: Attributes | None = None, context: Context | None = None, ) -> None: return super().set(amount, attributes=attributes, context=context) class _ProxyGauge( _ProxyInstrument[Gauge], Gauge, ): def set( self, amount: int | float, attributes: Attributes | None = None, context: Context | None = 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, )