From bbeb7c36b2c4a8c2a5f4d1ed690ef722eacde05f Mon Sep 17 00:00:00 2001 From: Chris Marchbanks Date: Tue, 8 Feb 2022 12:05:31 -0700 Subject: [PATCH 1/6] Add type annotations to samples.py --- prometheus_client/samples.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/prometheus_client/samples.py b/prometheus_client/samples.py index 0eda219f..b452d7ff 100644 --- a/prometheus_client/samples.py +++ b/prometheus_client/samples.py @@ -4,30 +4,30 @@ class Timestamp: """A nanosecond-resolution timestamp.""" - def __init__(self, sec, nsec): + def __init__(self, sec: Union[int, float], nsec: Union[int, float]) -> None: if nsec < 0 or nsec >= 1e9: raise ValueError(f"Invalid value for nanoseconds in Timestamp: {nsec}") if sec < 0: nsec = -nsec - self.sec = int(sec) - self.nsec = int(nsec) + self.sec: int = int(sec) + self.nsec: int = int(nsec) - def __str__(self): + def __str__(self) -> str: return f"{self.sec}.{self.nsec:09d}" - def __repr__(self): + def __repr__(self) -> str: return f"Timestamp({self.sec}, {self.nsec})" - def __float__(self): + def __float__(self) -> float: return float(self.sec) + float(self.nsec) / 1e9 - def __eq__(self, other): - return type(self) == type(other) and self.sec == other.sec and self.nsec == other.nsec + def __eq__(self, other: object) -> bool: + return isinstance(other, Timestamp) and self.sec == other.sec and self.nsec == other.nsec - def __ne__(self, other): + def __ne__(self, other: object) -> bool: return not self == other - def __gt__(self, other): + def __gt__(self, other: "Timestamp") -> bool: return self.sec > other.sec or self.nsec > other.nsec @@ -45,6 +45,6 @@ class Exemplar(NamedTuple): class Sample(NamedTuple): name: str labels: Dict[str, str] - value: float + value: Union[int, float] timestamp: Optional[Union[float, Timestamp]] = None exemplar: Optional[Exemplar] = None From d893b1c129ec38bf0bb8402cc643e4bf49b8efa6 Mon Sep 17 00:00:00 2001 From: Chris Marchbanks Date: Tue, 8 Feb 2022 12:07:15 -0700 Subject: [PATCH 2/6] Add typing to utils.py --- prometheus_client/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/prometheus_client/utils.py b/prometheus_client/utils.py index 0d2b0948..773f2c70 100644 --- a/prometheus_client/utils.py +++ b/prometheus_client/utils.py @@ -1,11 +1,12 @@ import math +from typing import Union INF = float("inf") MINUS_INF = float("-inf") NaN = float("NaN") -def floatToGoString(d): +def floatToGoString(d: Union[float, int, str]) -> str: d = float(d) if d == INF: return '+Inf' From a925066ade4d2cd54955bd4c8b1428de13c185f5 Mon Sep 17 00:00:00 2001 From: Chris Marchbanks Date: Tue, 8 Feb 2022 13:29:25 -0700 Subject: [PATCH 3/6] Add typing for values.py --- prometheus_client/values.py | 65 ++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/prometheus_client/values.py b/prometheus_client/values.py index 03b203be..c131f8a3 100644 --- a/prometheus_client/values.py +++ b/prometheus_client/values.py @@ -1,42 +1,85 @@ +from abc import ABC, abstractmethod import os from threading import Lock +from typing import Any, Callable, Optional, Sequence import warnings from .mmap_dict import mmap_key, MmapedDict +from .samples import Exemplar -class MutexValue: +class Value(ABC): + @abstractmethod + def __init__(self, + typ: Optional[str], + metric_name: str, + name: str, + labelnames: Sequence[str], + labelvalues: Sequence[str], + **kwargs: Any, + ): + pass + + @abstractmethod + def inc(self, amount: float) -> None: + pass + + @abstractmethod + def set(self, value: float) -> None: + pass + + @abstractmethod + def set_exemplar(self, exemplar: Exemplar) -> None: + pass + + @abstractmethod + def get(self) -> float: + pass + + @abstractmethod + def get_exemplar(self) -> Optional[Exemplar]: + pass + + +class MutexValue(Value): """A float protected by a mutex.""" _multiprocess = False - def __init__(self, typ, metric_name, name, labelnames, labelvalues, **kwargs): + def __init__(self, + typ: Optional[str], + metric_name: str, + name: str, + labelnames: Sequence[str], + labelvalues: Sequence[str], + **kwargs: Any, + ): self._value = 0.0 - self._exemplar = None + self._exemplar: Optional[Exemplar] = None self._lock = Lock() - def inc(self, amount): + def inc(self, amount: float) -> None: with self._lock: self._value += amount - def set(self, value): + def set(self, value: float) -> None: with self._lock: self._value = value - def set_exemplar(self, exemplar): + def set_exemplar(self, exemplar: Exemplar) -> None: with self._lock: self._exemplar = exemplar - def get(self): + def get(self) -> float: with self._lock: return self._value - def get_exemplar(self): + def get_exemplar(self) -> Optional[Exemplar]: with self._lock: return self._exemplar -def MultiProcessValue(process_identifier=os.getpid): +def MultiProcessValue(process_identifier: Callable[[], int] = os.getpid) -> type[Value]: """Returns a MmapedValue class based on a process_identifier function. The 'process_identifier' function MUST comply with this simple rule: @@ -52,7 +95,7 @@ def MultiProcessValue(process_identifier=os.getpid): # This avoids the need to also have mutexes in __MmapDict. lock = Lock() - class MmapedValue: + class MmapedValue(Value): """A float protected by a mutex backed by a per-process mmaped file.""" _multiprocess = True @@ -123,7 +166,7 @@ def get_exemplar(self): return MmapedValue -def get_value_class(): +def get_value_class() -> type[Value]: # Should we enable multi-process mode? # This needs to be chosen before the first metric is constructed, # and as that may be in some arbitrary library the user/admin has From 423dfa3ee6f6d0ad882027136930f3818c9efd0e Mon Sep 17 00:00:00 2001 From: Chris Marchbanks Date: Tue, 8 Feb 2022 13:44:18 -0700 Subject: [PATCH 4/6] Add typing to platform_collector.py --- prometheus_client/platform_collector.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/prometheus_client/platform_collector.py b/prometheus_client/platform_collector.py index c57d9fe8..1407b37f 100644 --- a/prometheus_client/platform_collector.py +++ b/prometheus_client/platform_collector.py @@ -1,13 +1,14 @@ import platform as pf +from typing import Any, Iterable, Optional -from .metrics_core import GaugeMetricFamily -from .registry import REGISTRY +from .metrics_core import GaugeMetricFamily, Metric +from .registry import CollectorRegistry, REGISTRY class PlatformCollector: """Collector for python platform information""" - def __init__(self, registry=REGISTRY, platform=None): + def __init__(self, registry: CollectorRegistry = REGISTRY, platform: Optional[Any] = None): self._platform = pf if platform is None else platform info = self._info() system = self._platform.system() @@ -19,7 +20,7 @@ def __init__(self, registry=REGISTRY, platform=None): if registry: registry.register(self) - def collect(self): + def collect(self) -> Iterable[Metric]: return self._metrics @staticmethod From e490cb8627da32bb00f3e9827ed54a6d35ab460c Mon Sep 17 00:00:00 2001 From: Chris Marchbanks Date: Tue, 8 Feb 2022 16:11:26 -0700 Subject: [PATCH 5/6] Add typing to metrics_core.py --- prometheus_client/metrics_core.py | 139 +++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 29 deletions(-) diff --git a/prometheus_client/metrics_core.py b/prometheus_client/metrics_core.py index a0408786..fea33a39 100644 --- a/prometheus_client/metrics_core.py +++ b/prometheus_client/metrics_core.py @@ -1,6 +1,7 @@ import re +from typing import Dict, List, Optional, Sequence, Tuple, Union -from .samples import Sample +from .samples import Exemplar, Sample, Timestamp METRIC_TYPES = ( 'counter', 'gauge', 'summary', 'histogram', @@ -20,36 +21,36 @@ class Metric: and SummaryMetricFamily instead. """ - def __init__(self, name, documentation, typ, unit=''): + def __init__(self, name: str, documentation: str, typ: str, unit: str = ''): if unit and not name.endswith("_" + unit): name += "_" + unit if not METRIC_NAME_RE.match(name): raise ValueError('Invalid metric name: ' + name) - self.name = name - self.documentation = documentation - self.unit = unit + self.name: str = name + self.documentation: str = documentation + self.unit: str = unit if typ == 'untyped': typ = 'unknown' if typ not in METRIC_TYPES: raise ValueError('Invalid metric type: ' + typ) - self.type = typ - self.samples = [] + self.type: str = typ + self.samples: List[Sample] = [] - def add_sample(self, name, labels, value, timestamp=None, exemplar=None): + def add_sample(self, name: str, labels: Dict[str, str], value: float, timestamp: Optional[Union[Timestamp, float]] = None, exemplar: Optional[Exemplar] = None) -> None: """Add a sample to the metric. Internal-only, do not use.""" self.samples.append(Sample(name, labels, value, timestamp, exemplar)) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return (isinstance(other, Metric) - and self.name == other.name + and self.name == other.name and self.documentation == other.documentation and self.type == other.type and self.unit == other.unit and self.samples == other.samples) - def __repr__(self): + def __repr__(self) -> str: return "Metric({}, {}, {}, {}, {})".format( self.name, self.documentation, @@ -73,7 +74,13 @@ class UnknownMetricFamily(Metric): For use by custom collectors. """ - def __init__(self, name, documentation, value=None, labels=None, unit=''): + def __init__(self, + name: str, + documentation: str, + value: Optional[float] = None, + labels: Optional[Sequence[str]] = None, + unit: str = '', + ): Metric.__init__(self, name, documentation, 'unknown', unit) if labels is not None and value is not None: raise ValueError('Can only specify at most one of value and labels.') @@ -83,7 +90,7 @@ def __init__(self, name, documentation, value=None, labels=None, unit=''): if value is not None: self.add_metric([], value) - def add_metric(self, labels, value, timestamp=None): + def add_metric(self, labels: Sequence[str], value: float, timestamp: Optional[Union[Timestamp, float]] = None) -> None: """Add a metric to the metric family. Args: labels: A list of label values @@ -102,7 +109,14 @@ class CounterMetricFamily(Metric): For use by custom collectors. """ - def __init__(self, name, documentation, value=None, labels=None, created=None, unit=''): + def __init__(self, + name: str, + documentation: str, + value: Optional[float] = None, + labels: Sequence[str] = None, + created: Optional[float] = None, + unit: str = '', + ): # Glue code for pre-OpenMetrics metrics. if name.endswith('_total'): name = name[:-6] @@ -115,7 +129,12 @@ def __init__(self, name, documentation, value=None, labels=None, created=None, u if value is not None: self.add_metric([], value, created) - def add_metric(self, labels, value, created=None, timestamp=None): + def add_metric(self, + labels: Sequence[str], + value: float, + created: Optional[float] = None, + timestamp: Optional[Union[Timestamp, float]] = None, + ) -> None: """Add a metric to the metric family. Args: @@ -134,7 +153,13 @@ class GaugeMetricFamily(Metric): For use by custom collectors. """ - def __init__(self, name, documentation, value=None, labels=None, unit=''): + def __init__(self, + name: str, + documentation: str, + value: Optional[float] = None, + labels: Optional[Sequence[str]] = None, + unit: str = '', + ): Metric.__init__(self, name, documentation, 'gauge', unit) if labels is not None and value is not None: raise ValueError('Can only specify at most one of value and labels.') @@ -144,7 +169,7 @@ def __init__(self, name, documentation, value=None, labels=None, unit=''): if value is not None: self.add_metric([], value) - def add_metric(self, labels, value, timestamp=None): + def add_metric(self, labels: Sequence[str], value: float, timestamp: Optional[Union[Timestamp, float]] = None) -> None: """Add a metric to the metric family. Args: @@ -160,7 +185,14 @@ class SummaryMetricFamily(Metric): For use by custom collectors. """ - def __init__(self, name, documentation, count_value=None, sum_value=None, labels=None, unit=''): + def __init__(self, + name: str, + documentation: str, + count_value: Optional[float] = None, + sum_value: Optional[float] = None, + labels: Optional[Sequence[str]] = None, + unit: str = '', + ): Metric.__init__(self, name, documentation, 'summary', unit) if (sum_value is None) != (count_value is None): raise ValueError('count_value and sum_value must be provided together.') @@ -169,10 +201,17 @@ def __init__(self, name, documentation, count_value=None, sum_value=None, labels if labels is None: labels = [] self._labelnames = tuple(labels) - if count_value is not None: + # The and clause is necessary only for typing, the above ValueError will raise if only one is set. + if count_value is not None and sum_value is not None: self.add_metric([], count_value, sum_value) - def add_metric(self, labels, count_value, sum_value, timestamp=None): + def add_metric(self, + labels: Sequence[str], + count_value: float, + sum_value: float, + timestamp: + Optional[Union[float, Timestamp]] = None + ) -> None: """Add a metric to the metric family. Args: @@ -190,7 +229,14 @@ class HistogramMetricFamily(Metric): For use by custom collectors. """ - def __init__(self, name, documentation, buckets=None, sum_value=None, labels=None, unit=''): + def __init__(self, + name: str, + documentation: str, + buckets: Optional[Sequence[Tuple[str, float, Optional[Exemplar]]]] = None, + sum_value: Optional[float] = None, + labels: Optional[Sequence[str]] = None, + unit: str = '', + ): Metric.__init__(self, name, documentation, 'histogram', unit) if sum_value is not None and buckets is None: raise ValueError('sum value cannot be provided without buckets.') @@ -202,7 +248,11 @@ def __init__(self, name, documentation, buckets=None, sum_value=None, labels=Non if buckets is not None: self.add_metric([], buckets, sum_value) - def add_metric(self, labels, buckets, sum_value, timestamp=None): + def add_metric(self, + labels: Sequence[str], + buckets: Sequence[Tuple[str, float, Optional[Exemplar]]], + sum_value: Optional[float], + timestamp: Optional[Union[Timestamp, float]] = None) -> None: """Add a metric to the metric family. Args: @@ -241,7 +291,14 @@ class GaugeHistogramMetricFamily(Metric): For use by custom collectors. """ - def __init__(self, name, documentation, buckets=None, gsum_value=None, labels=None, unit=''): + def __init__(self, + name: str, + documentation: str, + buckets: Optional[Sequence[Tuple[str, float]]] = None, + gsum_value: Optional[float] = None, + labels: Optional[Sequence[str]] = None, + unit: str = '', + ): Metric.__init__(self, name, documentation, 'gaugehistogram', unit) if labels is not None and buckets is not None: raise ValueError('Can only specify at most one of buckets and labels.') @@ -251,7 +308,12 @@ def __init__(self, name, documentation, buckets=None, gsum_value=None, labels=No if buckets is not None: self.add_metric([], buckets, gsum_value) - def add_metric(self, labels, buckets, gsum_value, timestamp=None): + def add_metric(self, + labels: Sequence[str], + buckets: Sequence[Tuple[str, float]], + gsum_value: Optional[float], + timestamp: Optional[Union[float, Timestamp]] = None, + ) -> None: """Add a metric to the metric family. Args: @@ -268,7 +330,8 @@ def add_metric(self, labels, buckets, gsum_value, timestamp=None): # +Inf is last and provides the count value. self.samples.extend([ Sample(self.name + '_gcount', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp), - Sample(self.name + '_gsum', dict(zip(self._labelnames, labels)), gsum_value, timestamp), + # TODO: Handle None gsum_value correctly. Currently a None will fail exposition but is allowed here. + Sample(self.name + '_gsum', dict(zip(self._labelnames, labels)), gsum_value, timestamp), # type: ignore ]) @@ -278,7 +341,12 @@ class InfoMetricFamily(Metric): For use by custom collectors. """ - def __init__(self, name, documentation, value=None, labels=None): + def __init__(self, + name: str, + documentation: str, + value: Optional[Dict[str, str]] = None, + labels: Optional[Sequence[str]] = None, + ): Metric.__init__(self, name, documentation, 'info') if labels is not None and value is not None: raise ValueError('Can only specify at most one of value and labels.') @@ -288,7 +356,11 @@ def __init__(self, name, documentation, value=None, labels=None): if value is not None: self.add_metric([], value) - def add_metric(self, labels, value, timestamp=None): + def add_metric(self, + labels: Sequence[str], + value: Dict[str, str], + timestamp: Optional[Union[Timestamp, float]] = None, + ) -> None: """Add a metric to the metric family. Args: @@ -309,7 +381,12 @@ class StateSetMetricFamily(Metric): For use by custom collectors. """ - def __init__(self, name, documentation, value=None, labels=None): + def __init__(self, + name: str, + documentation: str, + value: Optional[Dict[str, bool]] = None, + labels: Optional[Sequence[str]] = None, + ): Metric.__init__(self, name, documentation, 'stateset') if labels is not None and value is not None: raise ValueError('Can only specify at most one of value and labels.') @@ -319,7 +396,11 @@ def __init__(self, name, documentation, value=None, labels=None): if value is not None: self.add_metric([], value) - def add_metric(self, labels, value, timestamp=None): + def add_metric(self, + labels: Sequence[str], + value: Dict[str, bool], + timestamp: Optional[Union[Timestamp, float]] = None, + ) -> None: """Add a metric to the metric family. Args: From b8dc3bc179d320ad570d628a6e9998979d29b939 Mon Sep 17 00:00:00 2001 From: Chris Marchbanks Date: Tue, 8 Feb 2022 16:37:56 -0700 Subject: [PATCH 6/6] Typing in registry.py and metrics.py Also update built in collectors to use the new Abstract Base Class. --- prometheus_client/gc_collector.py | 15 ++++---- prometheus_client/metrics.py | 30 +++++++-------- prometheus_client/platform_collector.py | 4 +- prometheus_client/process_collector.py | 15 +++++--- prometheus_client/registry.py | 50 +++++++++++++++++-------- 5 files changed, 70 insertions(+), 44 deletions(-) diff --git a/prometheus_client/gc_collector.py b/prometheus_client/gc_collector.py index 0695670e..0a9a5fef 100644 --- a/prometheus_client/gc_collector.py +++ b/prometheus_client/gc_collector.py @@ -1,19 +1,20 @@ import gc import platform +from typing import Iterable -from .metrics_core import CounterMetricFamily -from .registry import REGISTRY +from .metrics_core import CounterMetricFamily, Metric +from .registry import Collector, REGISTRY, Registry -class GCCollector: +class GCCollector(Collector): """Collector for Garbage collection statistics.""" - def __init__(self, registry=REGISTRY): + def __init__(self, registry: Registry = REGISTRY): if not hasattr(gc, 'get_stats') or platform.python_implementation() != 'CPython': return registry.register(self) - def collect(self): + def collect(self) -> Iterable[Metric]: collected = CounterMetricFamily( 'python_gc_objects_collected', 'Objects collected during gc', @@ -31,8 +32,8 @@ def collect(self): labels=['generation'], ) - for generation, stat in enumerate(gc.get_stats()): - generation = str(generation) + for gen, stat in enumerate(gc.get_stats()): + generation = str(gen) collected.add_metric([generation], value=stat['collected']) uncollectable.add_metric([generation], value=stat['uncollectable']) collections.add_metric([generation], value=stat['collections']) diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index b9780d04..da0582e2 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -2,7 +2,7 @@ import time import types from typing import ( - Any, Callable, Dict, Iterable, Optional, Sequence, Type, TypeVar, Union, + Any, Callable, Dict, Iterable, List, Optional, Sequence, Type, TypeVar, Union, ) from . import values # retain this import style for testability @@ -11,7 +11,7 @@ Metric, METRIC_LABEL_NAME_RE, METRIC_NAME_RE, RESERVED_METRIC_LABEL_NAME_RE, ) -from .registry import CollectorRegistry, REGISTRY +from .registry import Collector, REGISTRY, Registry from .samples import Exemplar, Sample from .utils import floatToGoString, INF @@ -61,7 +61,7 @@ def _validate_exemplar(exemplar): raise ValueError('Exemplar labels have %d UTF-8 characters, exceeding the limit of 128') -class MetricWrapperBase: +class MetricWrapperBase(Collector): _type: Optional[str] = None _reserved_labelnames: Sequence[str] = () @@ -84,19 +84,19 @@ def _is_parent(self): def _get_metric(self): return Metric(self._name, self._documentation, self._type, self._unit) - def describe(self): + def describe(self) -> Iterable[Metric]: return [self._get_metric()] - def collect(self): + def collect(self) -> Iterable[Metric]: metric = self._get_metric() for suffix, labels, value, timestamp, exemplar in self._samples(): metric.add_sample(self._name + suffix, labels, value, timestamp, exemplar) return [metric] - def __str__(self): + def __str__(self) -> str: return f"{self._type}:{self._name}" - def __repr__(self): + def __repr__(self) -> str: metric_type = type(self) return f"{metric_type.__module__}.{metric_type.__name__}({self._name})" @@ -107,7 +107,7 @@ def __init__(self: T, namespace: str = '', subsystem: str = '', unit: str = '', - registry: Optional[CollectorRegistry] = REGISTRY, + registry: Optional[Registry] = REGISTRY, _labelvalues: Optional[Sequence[str]] = None, ) -> None: self._name = _build_full_name(self._type, name, namespace, subsystem, unit) @@ -188,7 +188,7 @@ def labels(self: T, *labelvalues: Any, **labelkwargs: Any) -> T: ) return self._metrics[labelvalues] - def remove(self, *labelvalues): + def remove(self, *labelvalues: str) -> None: if not self._labelnames: raise ValueError('No label names were set when constructing %s' % self) @@ -343,7 +343,7 @@ def __init__(self, namespace: str = '', subsystem: str = '', unit: str = '', - registry: Optional[CollectorRegistry] = REGISTRY, + registry: Optional[Registry] = REGISTRY, _labelvalues: Optional[Sequence[str]] = None, multiprocess_mode: str = 'all', ): @@ -536,7 +536,7 @@ def __init__(self, namespace: str = '', subsystem: str = '', unit: str = '', - registry: Optional[CollectorRegistry] = REGISTRY, + registry: Optional[Registry] = REGISTRY, _labelvalues: Optional[Sequence[str]] = None, buckets: Sequence[Union[float, int, str]] = DEFAULT_BUCKETS, ): @@ -566,7 +566,7 @@ def _prepare_buckets(self, source_buckets: Sequence[Union[float, int, str]]) -> self._upper_bounds = buckets def _metric_init(self) -> None: - self._buckets = [] + self._buckets: List[values.Value] = [] self._created = time.time() bucket_labelnames = self._labelnames + ('le',) self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues) @@ -608,7 +608,7 @@ def time(self) -> Timer: def _child_samples(self) -> Iterable[Sample]: samples = [] - acc = 0 + acc = 0.0 for i, bound in enumerate(self._upper_bounds): acc += self._buckets[i].get() samples.append(Sample('_bucket', {'le': floatToGoString(bound)}, acc, None, self._buckets[i].get_exemplar())) @@ -642,7 +642,7 @@ def _metric_init(self): self._lock = Lock() self._value = {} - def info(self, val): + def info(self, val: Dict[str, str]) -> None: """Set info metric.""" if self._labelname_set.intersection(val.keys()): raise ValueError('Overlapping labels for Info metric, metric: {} child: {}'.format( @@ -677,7 +677,7 @@ def __init__(self, namespace: str = '', subsystem: str = '', unit: str = '', - registry: Optional[CollectorRegistry] = REGISTRY, + registry: Optional[Registry] = REGISTRY, _labelvalues: Optional[Sequence[str]] = None, states: Optional[Sequence[str]] = None, ): diff --git a/prometheus_client/platform_collector.py b/prometheus_client/platform_collector.py index 1407b37f..f98e1930 100644 --- a/prometheus_client/platform_collector.py +++ b/prometheus_client/platform_collector.py @@ -2,10 +2,10 @@ from typing import Any, Iterable, Optional from .metrics_core import GaugeMetricFamily, Metric -from .registry import CollectorRegistry, REGISTRY +from .registry import Collector, CollectorRegistry, REGISTRY -class PlatformCollector: +class PlatformCollector(Collector): """Collector for python platform information""" def __init__(self, registry: CollectorRegistry = REGISTRY, platform: Optional[Any] = None): diff --git a/prometheus_client/process_collector.py b/prometheus_client/process_collector.py index e6271ba6..010bc92a 100644 --- a/prometheus_client/process_collector.py +++ b/prometheus_client/process_collector.py @@ -1,7 +1,8 @@ import os +from typing import Callable, Iterable, Union -from .metrics_core import CounterMetricFamily, GaugeMetricFamily -from .registry import REGISTRY +from .metrics_core import CounterMetricFamily, GaugeMetricFamily, Metric +from .registry import Collector, REGISTRY, Registry try: import resource @@ -12,10 +13,14 @@ _PAGESIZE = 4096 -class ProcessCollector: +class ProcessCollector(Collector): """Collector for Standard Exports such as cpu and memory.""" - def __init__(self, namespace='', pid=lambda: 'self', proc='/proc', registry=REGISTRY): + def __init__(self, + namespace: str = '', + pid: Callable[[], Union[int, str]] = lambda: 'self', + proc: str = '/proc', + registry: Registry = REGISTRY): self._namespace = namespace self._pid = pid self._proc = proc @@ -46,7 +51,7 @@ def _boot_time(self): if line.startswith(b'btime '): return float(line.split()[1]) - def collect(self): + def collect(self) -> Iterable[Metric]: if not self._btime: return [] diff --git a/prometheus_client/registry.py b/prometheus_client/registry.py index fe435cd1..f207a8a9 100644 --- a/prometheus_client/registry.py +++ b/prometheus_client/registry.py @@ -1,10 +1,30 @@ +from abc import ABC, abstractmethod import copy from threading import Lock +from typing import Dict, Iterable, List, Optional from .metrics_core import Metric -class CollectorRegistry: +# Ideally these would be Protocols, but Protocols are only available in Python >= 3.8. +class Collector(ABC): + @abstractmethod + def collect(self) -> Iterable[Metric]: + pass + + +class Registry(ABC): + @abstractmethod + def register(self, collector: Collector) -> None: + pass + + +class _EmptyCollector(Collector): + def collect(self) -> Iterable[Metric]: + return [] + + +class CollectorRegistry(Collector, Registry): """Metric collector registry. Collectors must have a no-argument method 'collect' that returns a list of @@ -12,15 +32,15 @@ class CollectorRegistry: exposition formats. """ - def __init__(self, auto_describe=False, target_info=None): - self._collector_to_names = {} - self._names_to_collectors = {} + def __init__(self, auto_describe: bool = False, target_info: Optional[Dict[str, str]] = None): + self._collector_to_names: Dict[Collector, List[str]] = {} + self._names_to_collectors: Dict[str, Collector] = {} self._auto_describe = auto_describe self._lock = Lock() - self._target_info = {} + self._target_info: Optional[Dict[str, str]] = {} self.set_target_info(target_info) - def register(self, collector): + def register(self, collector: Collector) -> None: """Add a collector to the registry.""" with self._lock: names = self._get_names(collector) @@ -33,7 +53,7 @@ def register(self, collector): self._names_to_collectors[name] = collector self._collector_to_names[collector] = names - def unregister(self, collector): + def unregister(self, collector: Collector) -> None: """Remove a collector from the registry.""" with self._lock: for name in self._collector_to_names[collector]: @@ -69,7 +89,7 @@ def _get_names(self, collector): result.append(metric.name + suffix) return result - def collect(self): + def collect(self) -> Iterable[Metric]: """Yields metrics from the collectors in the registry.""" collectors = None ti = None @@ -82,7 +102,7 @@ def collect(self): for collector in collectors: yield from collector.collect() - def restricted_registry(self, names): + def restricted_registry(self, names: Iterable[str]) -> "RestrictedRegistry": """Returns object that only collects some metrics. Returns an object which upon collect() will return @@ -95,17 +115,17 @@ def restricted_registry(self, names): names = set(names) return RestrictedRegistry(names, self) - def set_target_info(self, labels): + def set_target_info(self, labels: Optional[Dict[str, str]]) -> None: with self._lock: if labels: if not self._target_info and 'target_info' in self._names_to_collectors: raise ValueError('CollectorRegistry already contains a target_info metric') - self._names_to_collectors['target_info'] = None + self._names_to_collectors['target_info'] = _EmptyCollector() elif self._target_info: self._names_to_collectors.pop('target_info', None) self._target_info = labels - def get_target_info(self): + def get_target_info(self) -> Optional[Dict[str, str]]: with self._lock: return self._target_info @@ -114,7 +134,7 @@ def _target_info_metric(self): m.add_sample('target_info', self._target_info, 1) return m - def get_sample_value(self, name, labels=None): + def get_sample_value(self, name: str, labels: Optional[Dict[str, str]] = None) -> Optional[float]: """Returns the sample value, or None if not found. This is inefficient, and intended only for use in unittests. @@ -129,11 +149,11 @@ def get_sample_value(self, name, labels=None): class RestrictedRegistry: - def __init__(self, names, registry): + def __init__(self, names: Iterable[str], registry: CollectorRegistry): self._name_set = set(names) self._registry = registry - def collect(self): + def collect(self) -> Iterable[Metric]: collectors = set() target_info_metric = None with self._registry._lock: