Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/opentelemetry/metrics/_internal/__init__.py: 38%
181 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
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.
15# pylint: disable=too-many-ancestors
17"""
18The OpenTelemetry metrics API describes the classes used to generate
19metrics.
21The :class:`.MeterProvider` provides users access to the :class:`.Meter` which in
22turn is used to create :class:`.Instrument` objects. The :class:`.Instrument` objects are
23used to record measurements.
25This module provides abstract (i.e. unimplemented) classes required for
26metrics, and a concrete no-op implementation :class:`.NoOpMeter` that allows applications
27to use the API package alone without a supporting implementation.
29To get a meter, you need to provide the package name from which you are
30calling the meter APIs to OpenTelemetry by calling `MeterProvider.get_meter`
31with the calling instrumentation name and the version of your package.
33The following code shows how to obtain a meter using the global :class:`.MeterProvider`::
35 from opentelemetry.metrics import get_meter
37 meter = get_meter("example-meter")
38 counter = meter.create_counter("example-counter")
40.. versionadded:: 1.10.0
41"""
44from abc import ABC, abstractmethod
45from logging import getLogger
46from os import environ
47from threading import Lock
48from typing import List, Optional, Sequence, Set, Tuple, Union, cast
50from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER
51from opentelemetry.metrics._internal.instrument import (
52 CallbackT,
53 Counter,
54 Histogram,
55 NoOpCounter,
56 NoOpHistogram,
57 NoOpObservableCounter,
58 NoOpObservableGauge,
59 NoOpObservableUpDownCounter,
60 NoOpUpDownCounter,
61 ObservableCounter,
62 ObservableGauge,
63 ObservableUpDownCounter,
64 UpDownCounter,
65 _ProxyCounter,
66 _ProxyHistogram,
67 _ProxyObservableCounter,
68 _ProxyObservableGauge,
69 _ProxyObservableUpDownCounter,
70 _ProxyUpDownCounter,
71)
72from opentelemetry.util._once import Once
73from opentelemetry.util._providers import _load_provider
75_logger = getLogger(__name__)
78_ProxyInstrumentT = Union[
79 _ProxyCounter,
80 _ProxyHistogram,
81 _ProxyObservableCounter,
82 _ProxyObservableGauge,
83 _ProxyObservableUpDownCounter,
84 _ProxyUpDownCounter,
85]
88class MeterProvider(ABC):
89 """
90 MeterProvider is the entry point of the API. It provides access to `Meter` instances.
91 """
93 @abstractmethod
94 def get_meter(
95 self,
96 name: str,
97 version: Optional[str] = None,
98 schema_url: Optional[str] = None,
99 ) -> "Meter":
100 """Returns a `Meter` for use by the given instrumentation library.
102 For any two calls it is undefined whether the same or different
103 `Meter` instances are returned, even for different library names.
105 This function may return different `Meter` types (e.g. a no-op meter
106 vs. a functional meter).
108 Args:
109 name: The name of the instrumenting module.
110 ``__name__`` may not be used as this can result in
111 different meter names if the meters are in different files.
112 It is better to use a fixed string that can be imported where
113 needed and used consistently as the name of the meter.
115 This should *not* be the name of the module that is
116 instrumented but the name of the module doing the instrumentation.
117 E.g., instead of ``"requests"``, use
118 ``"opentelemetry.instrumentation.requests"``.
120 version: Optional. The version string of the
121 instrumenting library. Usually this should be the same as
122 ``pkg_resources.get_distribution(instrumenting_library_name).version``.
124 schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
125 """
128class NoOpMeterProvider(MeterProvider):
129 """The default MeterProvider used when no MeterProvider implementation is available."""
131 def get_meter(
132 self,
133 name: str,
134 version: Optional[str] = None,
135 schema_url: Optional[str] = None,
136 ) -> "Meter":
137 """Returns a NoOpMeter."""
138 super().get_meter(name, version=version, schema_url=schema_url)
139 return NoOpMeter(name, version=version, schema_url=schema_url)
142class _ProxyMeterProvider(MeterProvider):
143 def __init__(self) -> None:
144 self._lock = Lock()
145 self._meters: List[_ProxyMeter] = []
146 self._real_meter_provider: Optional[MeterProvider] = None
148 def get_meter(
149 self,
150 name: str,
151 version: Optional[str] = None,
152 schema_url: Optional[str] = None,
153 ) -> "Meter":
154 with self._lock:
155 if self._real_meter_provider is not None:
156 return self._real_meter_provider.get_meter(
157 name, version, schema_url
158 )
160 meter = _ProxyMeter(name, version=version, schema_url=schema_url)
161 self._meters.append(meter)
162 return meter
164 def on_set_meter_provider(self, meter_provider: MeterProvider) -> None:
165 with self._lock:
166 self._real_meter_provider = meter_provider
167 for meter in self._meters:
168 meter.on_set_meter_provider(meter_provider)
171class Meter(ABC):
172 """Handles instrument creation.
174 This class provides methods for creating instruments which are then
175 used to produce measurements.
176 """
178 def __init__(
179 self,
180 name: str,
181 version: Optional[str] = None,
182 schema_url: Optional[str] = None,
183 ) -> None:
184 super().__init__()
185 self._name = name
186 self._version = version
187 self._schema_url = schema_url
188 self._instrument_ids: Set[str] = set()
189 self._instrument_ids_lock = Lock()
191 @property
192 def name(self) -> str:
193 """
194 The name of the instrumenting module.
195 """
196 return self._name
198 @property
199 def version(self) -> Optional[str]:
200 """
201 The version string of the instrumenting library.
202 """
203 return self._version
205 @property
206 def schema_url(self) -> Optional[str]:
207 """
208 Specifies the Schema URL of the emitted telemetry
209 """
210 return self._schema_url
212 def _is_instrument_registered(
213 self, name: str, type_: type, unit: str, description: str
214 ) -> Tuple[bool, str]:
215 """
216 Check if an instrument with the same name, type, unit and description
217 has been registered already.
219 Returns a tuple. The first value is `True` if the instrument has been
220 registered already, `False` otherwise. The second value is the
221 instrument id.
222 """
224 instrument_id = ",".join(
225 [name.strip().lower(), type_.__name__, unit, description]
226 )
228 result = False
230 with self._instrument_ids_lock:
231 if instrument_id in self._instrument_ids:
232 result = True
233 else:
234 self._instrument_ids.add(instrument_id)
236 return (result, instrument_id)
238 @abstractmethod
239 def create_counter(
240 self,
241 name: str,
242 unit: str = "",
243 description: str = "",
244 ) -> Counter:
245 """Creates a `Counter` instrument
247 Args:
248 name: The name of the instrument to be created
249 unit: The unit for observations this instrument reports. For
250 example, ``By`` for bytes. UCUM units are recommended.
251 description: A description for this instrument and what it measures.
252 """
254 @abstractmethod
255 def create_up_down_counter(
256 self,
257 name: str,
258 unit: str = "",
259 description: str = "",
260 ) -> UpDownCounter:
261 """Creates an `UpDownCounter` instrument
263 Args:
264 name: The name of the instrument to be created
265 unit: The unit for observations this instrument reports. For
266 example, ``By`` for bytes. UCUM units are recommended.
267 description: A description for this instrument and what it measures.
268 """
270 @abstractmethod
271 def create_observable_counter(
272 self,
273 name: str,
274 callbacks: Optional[Sequence[CallbackT]] = None,
275 unit: str = "",
276 description: str = "",
277 ) -> ObservableCounter:
278 """Creates an `ObservableCounter` instrument
280 An observable counter observes a monotonically increasing count by calling provided
281 callbacks which accept a :class:`~opentelemetry.metrics.CallbackOptions` and return
282 multiple :class:`~opentelemetry.metrics.Observation`.
284 For example, an observable counter could be used to report system CPU
285 time periodically. Here is a basic implementation::
287 def cpu_time_callback(options: CallbackOptions) -> Iterable[Observation]:
288 observations = []
289 with open("/proc/stat") as procstat:
290 procstat.readline() # skip the first line
291 for line in procstat:
292 if not line.startswith("cpu"): break
293 cpu, *states = line.split()
294 observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"}))
295 observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}))
296 observations.append(Observation(int(states[2]) // 100, {"cpu": cpu, "state": "system"}))
297 # ... other states
298 return observations
300 meter.create_observable_counter(
301 "system.cpu.time",
302 callbacks=[cpu_time_callback],
303 unit="s",
304 description="CPU time"
305 )
307 To reduce memory usage, you can use generator callbacks instead of
308 building the full list::
310 def cpu_time_callback(options: CallbackOptions) -> Iterable[Observation]:
311 with open("/proc/stat") as procstat:
312 procstat.readline() # skip the first line
313 for line in procstat:
314 if not line.startswith("cpu"): break
315 cpu, *states = line.split()
316 yield Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"})
317 yield Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"})
318 # ... other states
320 Alternatively, you can pass a sequence of generators directly instead of a sequence of
321 callbacks, which each should return iterables of :class:`~opentelemetry.metrics.Observation`::
323 def cpu_time_callback(states_to_include: set[str]) -> Iterable[Iterable[Observation]]:
324 # accept options sent in from OpenTelemetry
325 options = yield
326 while True:
327 observations = []
328 with open("/proc/stat") as procstat:
329 procstat.readline() # skip the first line
330 for line in procstat:
331 if not line.startswith("cpu"): break
332 cpu, *states = line.split()
333 if "user" in states_to_include:
334 observations.append(Observation(int(states[0]) // 100, {"cpu": cpu, "state": "user"}))
335 if "nice" in states_to_include:
336 observations.append(Observation(int(states[1]) // 100, {"cpu": cpu, "state": "nice"}))
337 # ... other states
338 # yield the observations and receive the options for next iteration
339 options = yield observations
341 meter.create_observable_counter(
342 "system.cpu.time",
343 callbacks=[cpu_time_callback({"user", "system"})],
344 unit="s",
345 description="CPU time"
346 )
348 The :class:`~opentelemetry.metrics.CallbackOptions` contain a timeout which the
349 callback should respect. For example if the callback does asynchronous work, like
350 making HTTP requests, it should respect the timeout::
352 def scrape_http_callback(options: CallbackOptions) -> Iterable[Observation]:
353 r = requests.get('http://scrapethis.com', timeout=options.timeout_millis / 10**3)
354 for value in r.json():
355 yield Observation(value)
357 Args:
358 name: The name of the instrument to be created
359 callbacks: A sequence of callbacks that return an iterable of
360 :class:`~opentelemetry.metrics.Observation`. Alternatively, can be a sequence of generators that each
361 yields iterables of :class:`~opentelemetry.metrics.Observation`.
362 unit: The unit for observations this instrument reports. For
363 example, ``By`` for bytes. UCUM units are recommended.
364 description: A description for this instrument and what it measures.
365 """
367 @abstractmethod
368 def create_histogram(
369 self,
370 name: str,
371 unit: str = "",
372 description: str = "",
373 ) -> Histogram:
374 """Creates a :class:`~opentelemetry.metrics.Histogram` instrument
376 Args:
377 name: The name of the instrument to be created
378 unit: The unit for observations this instrument reports. For
379 example, ``By`` for bytes. UCUM units are recommended.
380 description: A description for this instrument and what it measures.
381 """
383 @abstractmethod
384 def create_observable_gauge(
385 self,
386 name: str,
387 callbacks: Optional[Sequence[CallbackT]] = None,
388 unit: str = "",
389 description: str = "",
390 ) -> ObservableGauge:
391 """Creates an `ObservableGauge` instrument
393 Args:
394 name: The name of the instrument to be created
395 callbacks: A sequence of callbacks that return an iterable of
396 :class:`~opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables
397 of :class:`~opentelemetry.metrics.Observation`.
398 unit: The unit for observations this instrument reports. For
399 example, ``By`` for bytes. UCUM units are recommended.
400 description: A description for this instrument and what it measures.
401 """
403 @abstractmethod
404 def create_observable_up_down_counter(
405 self,
406 name: str,
407 callbacks: Optional[Sequence[CallbackT]] = None,
408 unit: str = "",
409 description: str = "",
410 ) -> ObservableUpDownCounter:
411 """Creates an `ObservableUpDownCounter` instrument
413 Args:
414 name: The name of the instrument to be created
415 callbacks: A sequence of callbacks that return an iterable of
416 :class:`~opentelemetry.metrics.Observation`. Alternatively, can be a generator that yields iterables
417 of :class:`~opentelemetry.metrics.Observation`.
418 unit: The unit for observations this instrument reports. For
419 example, ``By`` for bytes. UCUM units are recommended.
420 description: A description for this instrument and what it measures.
421 """
424class _ProxyMeter(Meter):
425 def __init__(
426 self,
427 name: str,
428 version: Optional[str] = None,
429 schema_url: Optional[str] = None,
430 ) -> None:
431 super().__init__(name, version=version, schema_url=schema_url)
432 self._lock = Lock()
433 self._instruments: List[_ProxyInstrumentT] = []
434 self._real_meter: Optional[Meter] = None
436 def on_set_meter_provider(self, meter_provider: MeterProvider) -> None:
437 """Called when a real meter provider is set on the creating _ProxyMeterProvider
439 Creates a real backing meter for this instance and notifies all created
440 instruments so they can create real backing instruments.
441 """
442 real_meter = meter_provider.get_meter(
443 self._name, self._version, self._schema_url
444 )
446 with self._lock:
447 self._real_meter = real_meter
448 # notify all proxy instruments of the new meter so they can create
449 # real instruments to back themselves
450 for instrument in self._instruments:
451 instrument.on_meter_set(real_meter)
453 def create_counter(
454 self,
455 name: str,
456 unit: str = "",
457 description: str = "",
458 ) -> Counter:
459 with self._lock:
460 if self._real_meter:
461 return self._real_meter.create_counter(name, unit, description)
462 proxy = _ProxyCounter(name, unit, description)
463 self._instruments.append(proxy)
464 return proxy
466 def create_up_down_counter(
467 self,
468 name: str,
469 unit: str = "",
470 description: str = "",
471 ) -> UpDownCounter:
472 with self._lock:
473 if self._real_meter:
474 return self._real_meter.create_up_down_counter(
475 name, unit, description
476 )
477 proxy = _ProxyUpDownCounter(name, unit, description)
478 self._instruments.append(proxy)
479 return proxy
481 def create_observable_counter(
482 self,
483 name: str,
484 callbacks: Optional[Sequence[CallbackT]] = None,
485 unit: str = "",
486 description: str = "",
487 ) -> ObservableCounter:
488 with self._lock:
489 if self._real_meter:
490 return self._real_meter.create_observable_counter(
491 name, callbacks, unit, description
492 )
493 proxy = _ProxyObservableCounter(
494 name, callbacks, unit=unit, description=description
495 )
496 self._instruments.append(proxy)
497 return proxy
499 def create_histogram(
500 self,
501 name: str,
502 unit: str = "",
503 description: str = "",
504 ) -> Histogram:
505 with self._lock:
506 if self._real_meter:
507 return self._real_meter.create_histogram(
508 name, unit, description
509 )
510 proxy = _ProxyHistogram(name, unit, description)
511 self._instruments.append(proxy)
512 return proxy
514 def create_observable_gauge(
515 self,
516 name: str,
517 callbacks: Optional[Sequence[CallbackT]] = None,
518 unit: str = "",
519 description: str = "",
520 ) -> ObservableGauge:
521 with self._lock:
522 if self._real_meter:
523 return self._real_meter.create_observable_gauge(
524 name, callbacks, unit, description
525 )
526 proxy = _ProxyObservableGauge(
527 name, callbacks, unit=unit, description=description
528 )
529 self._instruments.append(proxy)
530 return proxy
532 def create_observable_up_down_counter(
533 self,
534 name: str,
535 callbacks: Optional[Sequence[CallbackT]] = None,
536 unit: str = "",
537 description: str = "",
538 ) -> ObservableUpDownCounter:
539 with self._lock:
540 if self._real_meter:
541 return self._real_meter.create_observable_up_down_counter(
542 name,
543 callbacks,
544 unit,
545 description,
546 )
547 proxy = _ProxyObservableUpDownCounter(
548 name, callbacks, unit=unit, description=description
549 )
550 self._instruments.append(proxy)
551 return proxy
554class NoOpMeter(Meter):
555 """The default Meter used when no Meter implementation is available.
557 All operations are no-op.
558 """
560 def create_counter(
561 self,
562 name: str,
563 unit: str = "",
564 description: str = "",
565 ) -> Counter:
566 """Returns a no-op Counter."""
567 super().create_counter(name, unit=unit, description=description)
568 if self._is_instrument_registered(
569 name, NoOpCounter, unit, description
570 )[0]:
571 _logger.warning(
572 "An instrument with name %s, type %s, unit %s and "
573 "description %s has been created already.",
574 name,
575 Counter.__name__,
576 unit,
577 description,
578 )
579 return NoOpCounter(name, unit=unit, description=description)
581 def create_up_down_counter(
582 self,
583 name: str,
584 unit: str = "",
585 description: str = "",
586 ) -> UpDownCounter:
587 """Returns a no-op UpDownCounter."""
588 super().create_up_down_counter(
589 name, unit=unit, description=description
590 )
591 if self._is_instrument_registered(
592 name, NoOpUpDownCounter, unit, description
593 )[0]:
594 _logger.warning(
595 "An instrument with name %s, type %s, unit %s and "
596 "description %s has been created already.",
597 name,
598 UpDownCounter.__name__,
599 unit,
600 description,
601 )
602 return NoOpUpDownCounter(name, unit=unit, description=description)
604 def create_observable_counter(
605 self,
606 name: str,
607 callbacks: Optional[Sequence[CallbackT]] = None,
608 unit: str = "",
609 description: str = "",
610 ) -> ObservableCounter:
611 """Returns a no-op ObservableCounter."""
612 super().create_observable_counter(
613 name, callbacks, unit=unit, description=description
614 )
615 if self._is_instrument_registered(
616 name, NoOpObservableCounter, unit, description
617 )[0]:
618 _logger.warning(
619 "An instrument with name %s, type %s, unit %s and "
620 "description %s has been created already.",
621 name,
622 ObservableCounter.__name__,
623 unit,
624 description,
625 )
626 return NoOpObservableCounter(
627 name,
628 callbacks,
629 unit=unit,
630 description=description,
631 )
633 def create_histogram(
634 self,
635 name: str,
636 unit: str = "",
637 description: str = "",
638 ) -> Histogram:
639 """Returns a no-op Histogram."""
640 super().create_histogram(name, unit=unit, description=description)
641 if self._is_instrument_registered(
642 name, NoOpHistogram, unit, description
643 )[0]:
644 _logger.warning(
645 "An instrument with name %s, type %s, unit %s and "
646 "description %s has been created already.",
647 name,
648 Histogram.__name__,
649 unit,
650 description,
651 )
652 return NoOpHistogram(name, unit=unit, description=description)
654 def create_observable_gauge(
655 self,
656 name: str,
657 callbacks: Optional[Sequence[CallbackT]] = None,
658 unit: str = "",
659 description: str = "",
660 ) -> ObservableGauge:
661 """Returns a no-op ObservableGauge."""
662 super().create_observable_gauge(
663 name, callbacks, unit=unit, description=description
664 )
665 if self._is_instrument_registered(
666 name, NoOpObservableGauge, unit, description
667 )[0]:
668 _logger.warning(
669 "An instrument with name %s, type %s, unit %s and "
670 "description %s has been created already.",
671 name,
672 ObservableGauge.__name__,
673 unit,
674 description,
675 )
676 return NoOpObservableGauge(
677 name,
678 callbacks,
679 unit=unit,
680 description=description,
681 )
683 def create_observable_up_down_counter(
684 self,
685 name: str,
686 callbacks: Optional[Sequence[CallbackT]] = None,
687 unit: str = "",
688 description: str = "",
689 ) -> ObservableUpDownCounter:
690 """Returns a no-op ObservableUpDownCounter."""
691 super().create_observable_up_down_counter(
692 name, callbacks, unit=unit, description=description
693 )
694 if self._is_instrument_registered(
695 name, NoOpObservableUpDownCounter, unit, description
696 )[0]:
697 _logger.warning(
698 "An instrument with name %s, type %s, unit %s and "
699 "description %s has been created already.",
700 name,
701 ObservableUpDownCounter.__name__,
702 unit,
703 description,
704 )
705 return NoOpObservableUpDownCounter(
706 name,
707 callbacks,
708 unit=unit,
709 description=description,
710 )
713_METER_PROVIDER_SET_ONCE = Once()
714_METER_PROVIDER: Optional[MeterProvider] = None
715_PROXY_METER_PROVIDER = _ProxyMeterProvider()
718def get_meter(
719 name: str,
720 version: str = "",
721 meter_provider: Optional[MeterProvider] = None,
722) -> "Meter":
723 """Returns a `Meter` for use by the given instrumentation library.
725 This function is a convenience wrapper for
726 `opentelemetry.metrics.MeterProvider.get_meter`.
728 If meter_provider is omitted the current configured one is used.
729 """
730 if meter_provider is None:
731 meter_provider = get_meter_provider()
732 return meter_provider.get_meter(name, version)
735def _set_meter_provider(meter_provider: MeterProvider, log: bool) -> None:
736 def set_mp() -> None:
737 global _METER_PROVIDER # pylint: disable=global-statement
738 _METER_PROVIDER = meter_provider
740 # gives all proxies real instruments off the newly set meter provider
741 _PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider)
743 did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp)
745 if log and not did_set:
746 _logger.warning("Overriding of current MeterProvider is not allowed")
749def set_meter_provider(meter_provider: MeterProvider) -> None:
750 """Sets the current global :class:`~.MeterProvider` object.
752 This can only be done once, a warning will be logged if any further attempt
753 is made.
754 """
755 _set_meter_provider(meter_provider, log=True)
758def get_meter_provider() -> MeterProvider:
759 """Gets the current global :class:`~.MeterProvider` object."""
761 if _METER_PROVIDER is None:
762 if OTEL_PYTHON_METER_PROVIDER not in environ.keys():
763 return _PROXY_METER_PROVIDER
765 meter_provider: MeterProvider = _load_provider( # type: ignore
766 OTEL_PYTHON_METER_PROVIDER, "meter_provider"
767 )
768 _set_meter_provider(meter_provider, log=False)
770 # _METER_PROVIDER will have been set by one thread
771 return cast("MeterProvider", _METER_PROVIDER)