instrument.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. # Copyright The OpenTelemetry Authors
  2. # SPDX-License-Identifier: Apache-2.0
  3. # pylint: disable=too-many-ancestors
  4. from abc import ABC, abstractmethod
  5. from collections.abc import Callable, Generator, Iterable, Sequence
  6. from dataclasses import dataclass
  7. from logging import getLogger
  8. from re import compile as re_compile
  9. from typing import (
  10. Generic,
  11. TypeVar,
  12. )
  13. # pylint: disable=unused-import; needed for typing and sphinx
  14. from opentelemetry import metrics
  15. from opentelemetry.context import Context
  16. from opentelemetry.metrics._internal.observation import Observation
  17. from opentelemetry.util.types import (
  18. Attributes,
  19. )
  20. _logger = getLogger(__name__)
  21. _name_regex = re_compile(r"[a-zA-Z][-_./a-zA-Z0-9]{0,254}")
  22. _unit_regex = re_compile(r"[\x00-\x7F]{0,63}")
  23. @dataclass(frozen=True)
  24. class _MetricsHistogramAdvisory:
  25. explicit_bucket_boundaries: Sequence[float] | None = None
  26. @dataclass(frozen=True)
  27. class CallbackOptions:
  28. """Options for the callback
  29. Args:
  30. timeout_millis: Timeout for the callback's execution. If the callback does asynchronous
  31. work (e.g. HTTP requests), it should respect this timeout.
  32. """
  33. timeout_millis: float = 10_000
  34. InstrumentT = TypeVar("InstrumentT", bound="Instrument")
  35. # pylint: disable=invalid-name
  36. CallbackT = (
  37. Callable[[CallbackOptions], Iterable[Observation]]
  38. | Generator[Iterable[Observation], CallbackOptions, None]
  39. )
  40. class Instrument(ABC):
  41. """Abstract class that serves as base for all instruments."""
  42. @abstractmethod
  43. def __init__(
  44. self,
  45. name: str,
  46. unit: str = "",
  47. description: str = "",
  48. ) -> None:
  49. pass
  50. @staticmethod
  51. def _check_name_unit_description(
  52. name: str, unit: str, description: str
  53. ) -> dict[str, str | None]:
  54. """
  55. Checks the following instrument name, unit and description for
  56. compliance with the spec.
  57. Returns a dict with keys "name", "unit" and "description", the
  58. corresponding values will be the checked strings or `None` if the value
  59. is invalid. If valid, the checked strings should be used instead of the
  60. original values.
  61. """
  62. result: dict[str, str | None] = {}
  63. if _name_regex.fullmatch(name) is not None:
  64. result["name"] = name
  65. else:
  66. result["name"] = None
  67. if unit is None:
  68. unit = ""
  69. if _unit_regex.fullmatch(unit) is not None:
  70. result["unit"] = unit
  71. else:
  72. result["unit"] = None
  73. if description is None:
  74. result["description"] = ""
  75. else:
  76. result["description"] = description
  77. return result
  78. class _ProxyInstrument(ABC, Generic[InstrumentT]):
  79. def __init__(
  80. self,
  81. name: str,
  82. unit: str = "",
  83. description: str = "",
  84. ) -> None:
  85. self._name = name
  86. self._unit = unit
  87. self._description = description
  88. self._real_instrument: InstrumentT | None = None
  89. def on_meter_set(self, meter: "metrics.Meter") -> None:
  90. """Called when a real meter is set on the creating _ProxyMeter"""
  91. # We don't need any locking on proxy instruments because it's OK if some
  92. # measurements get dropped while a real backing instrument is being
  93. # created.
  94. self._real_instrument = self._create_real_instrument(meter)
  95. @abstractmethod
  96. def _create_real_instrument(self, meter: "metrics.Meter") -> InstrumentT:
  97. """Create an instance of the real instrument. Implement this."""
  98. class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT]):
  99. def __init__(
  100. self,
  101. name: str,
  102. callbacks: Sequence[CallbackT] | None = None,
  103. unit: str = "",
  104. description: str = "",
  105. ) -> None:
  106. super().__init__(name, unit, description)
  107. self._callbacks = callbacks
  108. class Synchronous(Instrument):
  109. """Base class for all synchronous instruments"""
  110. class Asynchronous(Instrument):
  111. """Base class for all asynchronous instruments"""
  112. @abstractmethod
  113. def __init__(
  114. self,
  115. name: str,
  116. callbacks: Sequence[CallbackT] | None = None,
  117. unit: str = "",
  118. description: str = "",
  119. ) -> None:
  120. super().__init__(name, unit=unit, description=description)
  121. class Counter(Synchronous):
  122. """A Counter is a synchronous `Instrument` which supports non-negative increments."""
  123. @abstractmethod
  124. def add(
  125. self,
  126. amount: int | float,
  127. attributes: Attributes | None = None,
  128. context: Context | None = None,
  129. ) -> None:
  130. """Records an increment to the counter.
  131. Args:
  132. amount: The amount to increment the counter by. Must be non-negative.
  133. attributes: Optional set of attributes to associate with the measurement.
  134. context: Optional context to associate with the measurement. If not
  135. provided, the current context is used.
  136. """
  137. class NoOpCounter(Counter):
  138. """No-op implementation of `Counter`."""
  139. def __init__(
  140. self,
  141. name: str,
  142. unit: str = "",
  143. description: str = "",
  144. ) -> None:
  145. super().__init__(name, unit=unit, description=description)
  146. def add(
  147. self,
  148. amount: int | float,
  149. attributes: Attributes | None = None,
  150. context: Context | None = None,
  151. ) -> None:
  152. return super().add(amount, attributes=attributes, context=context)
  153. class _ProxyCounter(_ProxyInstrument[Counter], Counter):
  154. def add(
  155. self,
  156. amount: int | float,
  157. attributes: Attributes | None = None,
  158. context: Context | None = None,
  159. ) -> None:
  160. if self._real_instrument:
  161. self._real_instrument.add(amount, attributes, context)
  162. def _create_real_instrument(self, meter: "metrics.Meter") -> Counter:
  163. return meter.create_counter(
  164. self._name,
  165. self._unit,
  166. self._description,
  167. )
  168. class UpDownCounter(Synchronous):
  169. """An UpDownCounter is a synchronous `Instrument` which supports increments and decrements."""
  170. @abstractmethod
  171. def add(
  172. self,
  173. amount: int | float,
  174. attributes: Attributes | None = None,
  175. context: Context | None = None,
  176. ) -> None:
  177. """Records an increment or decrement to the counter.
  178. Unlike `Counter`, the ``amount`` may be negative, allowing the
  179. instrument to track values that go up and down (e.g. number of
  180. active requests, queue depth).
  181. Args:
  182. amount: The amount to add to the counter. May be positive or negative.
  183. attributes: Optional set of attributes to associate with the measurement.
  184. context: Optional context to associate with the measurement. If not
  185. provided, the current context is used.
  186. """
  187. class NoOpUpDownCounter(UpDownCounter):
  188. """No-op implementation of `UpDownCounter`."""
  189. def __init__(
  190. self,
  191. name: str,
  192. unit: str = "",
  193. description: str = "",
  194. ) -> None:
  195. super().__init__(name, unit=unit, description=description)
  196. def add(
  197. self,
  198. amount: int | float,
  199. attributes: Attributes | None = None,
  200. context: Context | None = None,
  201. ) -> None:
  202. return super().add(amount, attributes=attributes, context=context)
  203. class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter):
  204. def add(
  205. self,
  206. amount: int | float,
  207. attributes: Attributes | None = None,
  208. context: Context | None = None,
  209. ) -> None:
  210. if self._real_instrument:
  211. self._real_instrument.add(amount, attributes, context)
  212. def _create_real_instrument(self, meter: "metrics.Meter") -> UpDownCounter:
  213. return meter.create_up_down_counter(
  214. self._name,
  215. self._unit,
  216. self._description,
  217. )
  218. class ObservableCounter(Asynchronous):
  219. """An ObservableCounter is an asynchronous `Instrument` which reports monotonically
  220. increasing value(s) when the instrument is being observed.
  221. """
  222. class NoOpObservableCounter(ObservableCounter):
  223. """No-op implementation of `ObservableCounter`."""
  224. def __init__(
  225. self,
  226. name: str,
  227. callbacks: Sequence[CallbackT] | None = None,
  228. unit: str = "",
  229. description: str = "",
  230. ) -> None:
  231. super().__init__(
  232. name,
  233. callbacks,
  234. unit=unit,
  235. description=description,
  236. )
  237. class _ProxyObservableCounter(
  238. _ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter
  239. ):
  240. def _create_real_instrument(
  241. self, meter: "metrics.Meter"
  242. ) -> ObservableCounter:
  243. return meter.create_observable_counter(
  244. self._name,
  245. self._callbacks,
  246. self._unit,
  247. self._description,
  248. )
  249. class ObservableUpDownCounter(Asynchronous):
  250. """An ObservableUpDownCounter is an asynchronous `Instrument` which reports additive value(s) (e.g.
  251. the process heap size - it makes sense to report the heap size from multiple processes and sum them
  252. up, so we get the total heap usage) when the instrument is being observed.
  253. """
  254. class NoOpObservableUpDownCounter(ObservableUpDownCounter):
  255. """No-op implementation of `ObservableUpDownCounter`."""
  256. def __init__(
  257. self,
  258. name: str,
  259. callbacks: Sequence[CallbackT] | None = None,
  260. unit: str = "",
  261. description: str = "",
  262. ) -> None:
  263. super().__init__(
  264. name,
  265. callbacks,
  266. unit=unit,
  267. description=description,
  268. )
  269. class _ProxyObservableUpDownCounter(
  270. _ProxyAsynchronousInstrument[ObservableUpDownCounter],
  271. ObservableUpDownCounter,
  272. ):
  273. def _create_real_instrument(
  274. self, meter: "metrics.Meter"
  275. ) -> ObservableUpDownCounter:
  276. return meter.create_observable_up_down_counter(
  277. self._name,
  278. self._callbacks,
  279. self._unit,
  280. self._description,
  281. )
  282. class Histogram(Synchronous):
  283. """Histogram is a synchronous `Instrument` which can be used to report arbitrary values
  284. that are likely to be statistically meaningful. It is intended for statistics such as
  285. histograms, summaries, and percentile.
  286. """
  287. @abstractmethod
  288. def __init__(
  289. self,
  290. name: str,
  291. unit: str = "",
  292. description: str = "",
  293. explicit_bucket_boundaries_advisory: Sequence[float] | None = None,
  294. ) -> None:
  295. pass
  296. @abstractmethod
  297. def record(
  298. self,
  299. amount: int | float,
  300. attributes: Attributes | None = None,
  301. context: Context | None = None,
  302. ) -> None:
  303. """Records a measurement.
  304. Used to report measurements that are likely to be statistically
  305. meaningful, such as request durations, payload sizes, or any value
  306. for which a distribution (e.g. percentiles) is useful.
  307. Args:
  308. amount: The measurement to record. Should be non-negative in most
  309. cases; negative values are only meaningful when the histogram
  310. is used to track signed deltas.
  311. attributes: Optional set of attributes to associate with the measurement.
  312. context: Optional context to associate with the measurement. If not
  313. provided, the current context is used.
  314. """
  315. class NoOpHistogram(Histogram):
  316. """No-op implementation of `Histogram`."""
  317. def __init__(
  318. self,
  319. name: str,
  320. unit: str = "",
  321. description: str = "",
  322. explicit_bucket_boundaries_advisory: Sequence[float] | None = None,
  323. ) -> None:
  324. super().__init__(
  325. name,
  326. unit=unit,
  327. description=description,
  328. explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
  329. )
  330. def record(
  331. self,
  332. amount: int | float,
  333. attributes: Attributes | None = None,
  334. context: Context | None = None,
  335. ) -> None:
  336. return super().record(amount, attributes=attributes, context=context)
  337. class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram):
  338. def __init__(
  339. self,
  340. name: str,
  341. unit: str = "",
  342. description: str = "",
  343. explicit_bucket_boundaries_advisory: Sequence[float] | None = None,
  344. ) -> None:
  345. super().__init__(name, unit=unit, description=description)
  346. self._explicit_bucket_boundaries_advisory = (
  347. explicit_bucket_boundaries_advisory
  348. )
  349. def record(
  350. self,
  351. amount: int | float,
  352. attributes: Attributes | None = None,
  353. context: Context | None = None,
  354. ) -> None:
  355. if self._real_instrument:
  356. self._real_instrument.record(amount, attributes, context)
  357. def _create_real_instrument(self, meter: "metrics.Meter") -> Histogram:
  358. return meter.create_histogram(
  359. self._name,
  360. self._unit,
  361. self._description,
  362. explicit_bucket_boundaries_advisory=self._explicit_bucket_boundaries_advisory,
  363. )
  364. class ObservableGauge(Asynchronous):
  365. """Asynchronous Gauge is an asynchronous `Instrument` which reports non-additive value(s) (e.g.
  366. the room temperature - it makes no sense to report the temperature value from multiple rooms
  367. and sum them up) when the instrument is being observed.
  368. """
  369. class NoOpObservableGauge(ObservableGauge):
  370. """No-op implementation of `ObservableGauge`."""
  371. def __init__(
  372. self,
  373. name: str,
  374. callbacks: Sequence[CallbackT] | None = None,
  375. unit: str = "",
  376. description: str = "",
  377. ) -> None:
  378. super().__init__(
  379. name,
  380. callbacks,
  381. unit=unit,
  382. description=description,
  383. )
  384. class _ProxyObservableGauge(
  385. _ProxyAsynchronousInstrument[ObservableGauge],
  386. ObservableGauge,
  387. ):
  388. def _create_real_instrument(
  389. self, meter: "metrics.Meter"
  390. ) -> ObservableGauge:
  391. return meter.create_observable_gauge(
  392. self._name,
  393. self._callbacks,
  394. self._unit,
  395. self._description,
  396. )
  397. class Gauge(Synchronous):
  398. """A Gauge is a synchronous `Instrument` which can be used to record non-additive values as they occur."""
  399. @abstractmethod
  400. def set(
  401. self,
  402. amount: int | float,
  403. attributes: Attributes | None = None,
  404. context: Context | None = None,
  405. ) -> None:
  406. """Records the current value of the gauge.
  407. The gauge reports the last recorded value when observed. It is
  408. intended for non-additive measurements where only the current
  409. value matters (e.g. CPU utilisation percentage, room temperature).
  410. Args:
  411. amount: The current value to record.
  412. attributes: Optional set of attributes to associate with the measurement.
  413. context: Optional context to associate with the measurement. If not
  414. provided, the current context is used.
  415. """
  416. class NoOpGauge(Gauge):
  417. """No-op implementation of ``Gauge``."""
  418. def __init__(
  419. self,
  420. name: str,
  421. unit: str = "",
  422. description: str = "",
  423. ) -> None:
  424. super().__init__(name, unit=unit, description=description)
  425. def set(
  426. self,
  427. amount: int | float,
  428. attributes: Attributes | None = None,
  429. context: Context | None = None,
  430. ) -> None:
  431. return super().set(amount, attributes=attributes, context=context)
  432. class _ProxyGauge(
  433. _ProxyInstrument[Gauge],
  434. Gauge,
  435. ):
  436. def set(
  437. self,
  438. amount: int | float,
  439. attributes: Attributes | None = None,
  440. context: Context | None = None,
  441. ) -> None:
  442. if self._real_instrument:
  443. self._real_instrument.set(amount, attributes, context)
  444. def _create_real_instrument(self, meter: "metrics.Meter") -> Gauge:
  445. return meter.create_gauge(
  446. self._name,
  447. self._unit,
  448. self._description,
  449. )