instrument.py 14 KB

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