complete rewrite

This commit is contained in:
Stefan Bühler 2025-05-09 17:12:57 +02:00
parent 65cb5d31f3
commit ef33d3e38b
8 changed files with 376 additions and 329 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Stefan Bühler (University of Stuttgart)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -4,14 +4,14 @@ build-backend = "flit_core.buildapi"
[project] [project]
name = "prometheus_rus" name = "prometheus_rus"
version = "0.1.0" version = "0.2.0"
authors = [ authors = [
{name = "Stefan Bühler", email = "stefan.buehler@tik.uni-stuttgart.de"}, {name = "Stefan Bühler", email = "stefan.buehler@tik.uni-stuttgart.de"},
] ]
license = "Apache Software License 2.0" license = {file = "LICENSE"}
classifiers = [ classifiers = [
"Private :: Do Not Upload", "Private :: Do Not Upload",
"License :: OSI Approved :: Apache Software License", "License :: OSI Approved :: MIT License",
] ]
dynamic = ["version", "description"] dynamic = ["version", "description"]

View File

@ -1,13 +1,19 @@
from ._registry import ( # noqa: reexport from ._registry import ( # noqa: reexport
Registry as Registry, Registry as Registry,
Path as Path,
GLOBAL_REGISTRY as GLOBAL_REGISTRY,
) )
from ._metrics import ( # noqa: reexport from ._metrics import ( # noqa: reexport
Counter as Counter, Counter as Counter,
CounterFamily as CounterFamily,
Gauge as Gauge, Gauge as Gauge,
GaugeFamily as GaugeFamily,
Summary as Summary, Summary as Summary,
SummaryFamily as SummaryFamily,
) )
from ._metric_base import ( # noqa: reexport from ._metric_base import ( # noqa: reexport
DynamicTimestamp as DynamicTimestamp,
MetricFamily,
MetricPoint,
MetricValueSet,
Now as Now,
NOW as NOW, NOW as NOW,
) )

View File

@ -1,83 +1,92 @@
from threading import Lock from __future__ import annotations
import typing
import time
import abc import abc
import dataclasses
import time
import typing
from ._path import Path from ._path import (
Labels,
LabelsData,
)
class Now(typing.NamedTuple): class Now:
__slots__ = ()
def time(self) -> int: def time(self) -> int:
"""Time since epoch in milliseconds"""
return round(time.time() * 1000) return round(time.time() * 1000)
NOW = Now() NOW = Now()
DynamicTimestamp = int | Now | None
class MetricValue(typing.NamedTuple):
value: float
timestamp: typing.Optional[int]
_NAN = float('nan') def _to_timestamp(timestamp: DynamicTimestamp) -> int | None:
class MetricMutableValue:
# global lock for metrics by default
DEFAULT_LOCK: typing.ClassVar[Lock] = Lock()
def __init__(self, value: float = _NAN, timestamp: typing.Optional[int] = None, lock: Lock = DEFAULT_LOCK):
self._lock = lock
self._value = MetricValue(value, timestamp)
def set_no_lock(self, value: float, timestamp: typing.Optional[int]):
self._value = MetricValue(value, timestamp)
def set(self, value: float, timestamp: typing.Union[int, None, Now] = NOW):
if isinstance(timestamp, Now): if isinstance(timestamp, Now):
timestamp = timestamp.time() return timestamp.time()
with self._lock: else:
self.set_no_lock(value, timestamp) return timestamp
def inc_no_lock(self, add: float, timestamp: typing.Optional[int]):
self._value = MetricValue(self._value.value + add, timestamp)
def inc(self, add: float = 1, timestamp: typing.Union[int, None, Now] = NOW):
if isinstance(timestamp, Now):
timestamp = timestamp.time()
with self._lock:
self.inc_no_lock(add, timestamp)
def get(self) -> MetricValue:
with self._lock:
return self._value
class MetricBase(MetricMutableValue): @dataclasses.dataclass(slots=True, kw_only=True)
metric_type: typing.ClassVar[str] class MetricValueSet:
help_text: typing.Optional[str] = None # list of suffixes and values; suffixes must match the allowed suffixes of the family of the point
values: list[tuple[str, float]]
def __init__(self, value: float = _NAN, timestamp: typing.Optional[int] = None, help: typing.Optional[str] = None): timestamp: int | None
super().__init__(value, timestamp)
self.help_text = help
class MetricGroupDefinition(typing.NamedTuple): _TMetricPoint = typing.TypeVar('_TMetricPoint', bound='MetricPoint')
reserved_suffixes: typing.FrozenSet[str]
reserved_labels: typing.FrozenSet[str]
EMPTY_GROUP_DEFINITION = MetricGroupDefinition(frozenset(), frozenset()) class MetricPoint(abc.ABC):
# _family and _labels set by MetricFamily._init_point
# __weakref__ is needed for weakref support
__slots__ = ('_family', '_labels', '__weakref__')
_family: MetricFamily
_labels: Labels
class MetricGroupBase(abc.ABC): @property
metric_type: typing.ClassVar[str] def family(self) -> MetricFamily:
group_definition: typing.ClassVar[MetricGroupDefinition] return self._family
help_text: typing.Optional[str] = None
def __init__(self, help: typing.Optional[str] = None): @property
self.help_text = help def labels(self) -> Labels:
return self._labels
def __repr__(self) -> str:
return f"<Point {self._family.name}{self._labels.key}>"
@abc.abstractmethod @abc.abstractmethod
def metrics(self) -> typing.Iterator[typing.Tuple[(Path, MetricMutableValue)]]: def _get_value_set(self) -> MetricValueSet:
pass ...
class MetricFamily(abc.ABC, typing.Generic[_TMetricPoint]):
TYPE: typing.ClassVar[str]
SUFFIXES: typing.ClassVar[list[str]] = ['']
RESERVED_LABELS: typing.ClassVar[list[str]] = []
def __init__(
self,
*,
name: str,
unit: str = "",
help: str = "",
) -> None:
self.name = name
self.unit = unit
self.help = help
def _init_point(self, labels: LabelsData, point: _TMetricPoint) -> _TMetricPoint:
if not isinstance(labels, Labels):
labels = Labels(labels)
for reserved_label in self.RESERVED_LABELS:
if reserved_label in labels.map:
raise KeyError(f"Point {labels.key} uses reserved label {reserved_label}")
point._family = self
point._labels = labels
return point

View File

@ -1,56 +1,145 @@
from __future__ import annotations
import contextlib
import dataclasses
import threading
import typing import typing
from ._metric_base import MetricBase, MetricGroupBase, MetricGroupDefinition, MetricMutableValue, Now, NOW from ._metric_base import (
from ._registry import Path, GLOBAL_REGISTRY _to_timestamp,
DynamicTimestamp,
MetricFamily,
MetricPoint,
MetricValueSet,
)
from ._path import LabelsData
class Counter(MetricBase): @contextlib.contextmanager
metric_type: typing.ClassVar[str] = 'counter' def opt_lock(lock: threading.Lock | None) -> typing.Iterator[None]:
if lock:
def __init__(self, value: float = 0, path: typing.Optional[Path] = None, help: typing.Optional[str] = None): with lock:
super().__init__(value=value, help=help) yield
if not path is None: else:
GLOBAL_REGISTRY.register(path, self) yield
class Gauge(MetricBase): @dataclasses.dataclass(slots=True, kw_only=True)
metric_type: typing.ClassVar[str] = 'gauge' class _NumberValue:
value: float = 0
timestamp: int | None = None
def __init__( def _get_value_set(self) -> MetricValueSet:
self, return MetricValueSet(
value: float = float('nan'), values=[("", self.value)],
path: typing.Optional[Path] = None, timestamp=self.timestamp,
help: typing.Optional[str] = None,
):
super().__init__(value=value, help=help)
if not path is None:
GLOBAL_REGISTRY.register(path, self)
class Summary(MetricGroupBase):
metric_type: typing.ClassVar[str] = 'summary'
group_definition: typing.ClassVar[MetricGroupDefinition] = MetricGroupDefinition(
frozenset(['_sum', '_count']),
frozenset(['quantile']),
) )
def __init__(self, path: typing.Optional[Path] = None, help: typing.Optional[str] = None):
super().__init__(help=help)
self._lock = MetricMutableValue.DEFAULT_LOCK
self._sum = MetricMutableValue(0, None, self._lock)
self._count = MetricMutableValue(0, None, self._lock)
if not path is None:
GLOBAL_REGISTRY.register(path, self)
def metrics(self) -> typing.Iterator[typing.Tuple[(Path, MetricMutableValue)]]: class _SimpleNumber(MetricPoint):
return [ __slots__ = ('_inc_lock', '_value')
(Path('_sum'), self._sum),
(Path('_count'), self._count),
]
def observe(self, value: float, timestamp: typing.Union[int, None, Now] = NOW): def __init__(self, *, value: float, inc_lock: threading.Lock | None) -> None:
if isinstance(timestamp, Now): super().__init__()
timestamp = timestamp.time() self._inc_lock = inc_lock
with self._lock: self._value = _NumberValue(value=value)
self._count.inc_no_lock(1, timestamp)
self._sum.inc_no_lock(value, timestamp) def _get_value_set(self) -> MetricValueSet:
return self._value._get_value_set()
def set(self, value: float, *, timestamp: DynamicTimestamp = None) -> None:
self._value = _NumberValue(value=value, timestamp=_to_timestamp(timestamp))
def inc_no_lock(self, add: float = 1, *, timestamp: DynamicTimestamp = None) -> None:
self._value = _NumberValue(value=self._value.value + add, timestamp=_to_timestamp(timestamp))
def inc(self, add: float = 1, *, timestamp: DynamicTimestamp = None) -> None:
timestamp = _to_timestamp(timestamp)
if self._inc_lock:
with self._inc_lock:
self.inc_no_lock(add=add, timestamp=timestamp)
else:
self.inc_no_lock(add=add, timestamp=timestamp)
class Counter(_SimpleNumber):
__slots__ = ()
def __init__(self, *, value: int, inc_lock: threading.Lock | None) -> None:
super().__init__(value=value, inc_lock=inc_lock)
class CounterFamily(MetricFamily[Counter]):
__slots__ = ()
TYPE: typing.ClassVar[str] = 'counter'
SUFFIXES: typing.ClassVar[list[str]] = ['', '_created']
DEFAULT_INC_LOCK: typing.ClassVar[threading.Lock] = threading.Lock()
def create(self, *, value: int = 0, labels: LabelsData) -> Counter:
point = Counter(value=value, inc_lock=self.DEFAULT_INC_LOCK)
return self._init_point(labels, point)
class Gauge(_SimpleNumber):
__slots__ = ()
def __init__(self, *, value: float, inc_lock: threading.Lock | None) -> None:
super().__init__(value=value, inc_lock=inc_lock)
class GaugeFamily(MetricFamily[Gauge]):
__slots__ = ()
TYPE: typing.ClassVar[str] = 'gauge'
SUFFIXES: typing.ClassVar[list[str]] = ['']
DEFAULT_INC_LOCK: typing.ClassVar[threading.Lock] = threading.Lock()
def create(self, *, value: float = 0, labels: LabelsData) -> Gauge:
point = Gauge(value=value, inc_lock=self.DEFAULT_INC_LOCK)
return self._init_point(labels, point)
class Summary(MetricPoint):
# TODO: quantile support
__slots__ = ('_lock', '_sum', '_count', '_timestamp')
def __init__(self, *, lock: threading.Lock | None = None) -> None:
super().__init__()
self._lock = lock
self._sum: float = 0
self._count: int = 0
self._timestamp: int | None = 0
def _get_value_set(self) -> MetricValueSet:
with opt_lock(self._lock):
return MetricValueSet(
values=[
("_sum", self._sum),
("_count", self._count),
],
timestamp=self._timestamp,
)
def observe(self, value: float, timestamp: DynamicTimestamp = None) -> None:
timestamp = _to_timestamp(timestamp)
with opt_lock(self._lock):
self._sum += value
self._count += 1
self._timestamp = timestamp
class SummaryFamily(MetricFamily[Summary]):
__slots__ = ()
TYPE: typing.ClassVar[str] = 'summary'
SUFFIXES: typing.ClassVar[list[str]] = ['', '_count', '_sum', '_created']
RESERVED_LABELS: typing.ClassVar[list[str]] = ['quantile']
DEFAULT_LOCK: typing.ClassVar[threading.Lock] = threading.Lock()
def create(self, *, value: float = float('nan'), labels: LabelsData) -> Summary:
point = Summary(lock=self.DEFAULT_LOCK)
return self._init_point(labels, point)

View File

@ -2,31 +2,48 @@ import typing
class Labels: class Labels:
def __init__(self, map: typing.Mapping[str, str]): __slots__ = ('_map', '_key')
def __init__(self, map: typing.Mapping[str, str]) -> None:
self._map = map self._map = map
self._key = '{{{0}}}'.format(','.join( self._key = '{{{0}}}'.format(','.join([
['{0}="{1}"'.format( '{0}="{1}"'.format(
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')
for k, v in sorted(map.items())])) )
for k, v in sorted(map.items())
]))
@property @property
def key(self) -> str: def key(self) -> str:
"""Metric labels as {...} string to use as suffix for the metric name in prometheus output"""
return self._key return self._key
@property @property
def map(self) -> typing.Mapping[str, str]: def map(self) -> typing.Mapping[str, str]:
"""Original mapping key got constructed from"""
return self._map return self._map
LabelsData = Labels | typing.Mapping[str, str]
class Path: class Path:
def __init__(self, name, labels={}): __slots__ = ('_name', '_labels')
def __init__(
self,
name: str,
labels: typing.Mapping[str, str] = {},
) -> None:
self._name = name self._name = name
self._labels = Labels(labels) self._labels = Labels(labels)
@property @property
def name(self) -> str: def name(self) -> str:
"""Metric name"""
return self._name return self._name
@property @property
def labels(self) -> Labels: def labels(self) -> Labels:
"""Metric labels"""
return self._labels return self._labels

View File

@ -1,90 +1,22 @@
from threading import Lock from __future__ import annotations
import math
import threading
import typing import typing
import weakref import weakref
import math
from ._metric_base import MetricValue, MetricBase, MetricGroupDefinition, MetricGroupBase, EMPTY_GROUP_DEFINITION from ._metric_base import (
from ._path import Path, Labels MetricFamily,
MetricPoint,
MetricValueSet,
class _MetricGroup: )
metrics: typing.Mapping[Labels, typing.Union[MetricBase, MetricGroupBase]]
weak_metric: typing.Any
def __init__(self, metrics: typing.Mapping[Labels, typing.Union[MetricBase, MetricGroupBase]], weak_metric):
self.metrics = metrics
self.weak_metric = weak_metric
def get(self) -> typing.Sequence[typing.Tuple[Path, MetricValue]]:
return [(k, m.get()) for (k, m) in self.metrics.items()]
class _MetricCollection:
metric_type: str
help_text: typing.Optional[str]
metrics: typing.MutableMapping[Labels, typing.Union[MetricBase, _MetricGroup]]
group_definition: typing.Optional[MetricGroupDefinition]
_groups: typing.MutableSet[_MetricGroup]
def __init__(self, metric_type: str, group_definition: typing.Optional[MetricGroupDefinition] = None):
self.metric_type = metric_type
self.help_text = None
self.metrics = weakref.WeakValueDictionary()
if group_definition is None:
group_definition = EMPTY_GROUP_DEFINITION
self.group_definition = group_definition
self._groups = set()
def check(self, path: Path, metric_type: str, group_definition: typing.Optional[MetricGroupDefinition] = None):
if self.metric_type != metric_type:
raise RuntimeError("metric type doesn't match for {}: {} != {}".format(
path.name, self.metric_type, metric_type))
if group_definition is None:
group_definition = EMPTY_GROUP_DEFINITION
if self.group_definition.reserved_suffixes != group_definition.reserved_suffixes:
raise RuntimeError("reserved suffixes don't match for {}: {} != {}".format(
path.name, self.group_definition.reserved_suffixes, group_definition.reserved_suffixes))
if self.group_definition.reserved_labels != group_definition.reserved_labels:
raise RuntimeError("reserved labels don't match for {}: {} != {}".format(
path.name, self.group_definition.reserved_labels, group_definition.reserved_labels))
for label in self.group_definition.reserved_labels:
if label in path.labels.map:
raise RuntimeError("'{} {}' uses reserved label {}".format(
path.name, path.labels.key, label))
if path.labels.key in self.metrics:
raise RuntimeError("'{} {}' already registered".format(
path.name, path.labels.key))
def insert(self, path: Path, metric: typing.Union[MetricBase, MetricGroupBase]):
if isinstance(metric, MetricBase):
self.metrics[path.labels.key] = metric
else:
key = path.labels.key
metrics = dict()
for (add_path, m) in metric.metrics():
assert (
add_path.name == '' or add_path.name in self.group_definition.reserved_suffixes)
for label in add_path.labels.map:
assert label in self.group_definition.reserved_labels
p = Path(path.name + add_path.name, {**path.labels.map, **add_path.labels.map})
metrics[p] = m
g = None # set below, but need for cleanup
def cleanup(_weak_metric):
self._groups.remove(g)
del self.metrics[key]
weak_metric = weakref.ref(metric, cleanup)
g = _MetricGroup(metrics, weak_metric)
self.metrics[key] = g
if self.help_text is None:
self.help_text = metric.help_text
_INF = float("inf") _INF = float("inf")
_MINUS_INF = float("-inf") _MINUS_INF = float("-inf")
def _floatToGoString(d: float): def _floatToGoString(d: float) -> str:
if d == _INF: if d == _INF:
return '+Inf' return '+Inf'
elif d == _MINUS_INF: elif d == _MINUS_INF:
@ -103,141 +35,100 @@ def _floatToGoString(d: float):
return s return s
class _FamilyInstance:
def __init__(self, family: MetricFamily) -> None:
self._family = family
self._lock = threading.Lock()
self._points: weakref.WeakValueDictionary[str, MetricPoint] = weakref.WeakValueDictionary()
def _add_point(self, point: MetricPoint) -> None:
key = point.labels.key
if not point.family is self._family:
raise KeyError(f"Point {point.family.name}{key} has different family {point.family} than {self._family}")
with self._lock:
if key in self._points:
raise KeyError(f"Point {key} already exists in family instance {self._family.name}")
self._points[key] = point
def _get_value_sets(self) -> list[tuple[str, MetricValueSet]]:
return [
(point.labels.key, point._get_value_set())
for point in self._points.values()
]
class MetricGroup(typing.Protocol):
def metric_points(self) -> typing.Iterable[MetricPoint]:
...
class Registry: class Registry:
_metrics: typing.MutableMapping[str, typing.Union[_MetricCollection, str]] def __init__(self) -> None:
_paths: typing.MutableMapping[typing.Union[MetricBase, MetricGroupBase], Path] self._lock = threading.Lock()
_strong_metrics: typing.MutableSet[typing.Union[MetricBase, MetricGroupBase]] self._families: dict[str, _FamilyInstance] = {}
self._names: dict[str, _FamilyInstance] = {}
def __init__(self): def _register_family(self, family: MetricFamily) -> _FamilyInstance:
self._lock = Lock()
self._metrics = dict()
self._paths = weakref.WeakKeyDictionary()
self._strong_metrics = set()
# requires lock
def _unregister(self, metric: MetricBase):
# don't keep alive anymore
try:
self._strong_metrics.remove(metric)
except KeyError:
pass
try:
path = self._paths[metric]
c = self._metrics[path.name]
del c[path.labels]
except KeyError:
pass
def unregister(self, metric: typing.Union[MetricBase, MetricGroupBase]):
with self._lock: with self._lock:
if isinstance(metric, MetricGroupBase): if family.name in self._families:
metrics = metric.metrics(Path("")) instance = self._families[family.name]
for (_, m) in metrics: if instance._family is family:
self._unregister(m) return instance
else: raise KeyError(f"Metric family {family.name} already reserved")
self._unregister(metric) if family.name in self._names:
raise KeyError(f"Metric family {family.name} already reserved by {self._names[family.name]._family}")
for suffix in family.SUFFIXES:
full_name = family.name + suffix
if full_name in self._families:
raise KeyError(f"Metric name {full_name} already reserved by other family")
if full_name in self._names:
raise KeyError(f"Metric name {full_name} already reserved by {self._names[full_name]._family}")
# now register
instance = _FamilyInstance(family=family)
self._families[family.name] = instance
for suffix in family.SUFFIXES:
full_name = family.name + suffix
self._names[full_name] = instance
return instance
# requires lock def register(self, point: MetricPoint) -> None:
def _remove_collection(self, metric_name: str): instance = self._register_family(point.family)
try: instance._add_point(point)
c = self._metrics[metric_name]
except KeyError:
return
assert isinstance(
c, _MetricCollection), "cannot remove reserved metric names directly"
for suffix in c.reserved_suffixes:
assert isinstance(
self._metrics[metric_name + suffix], str), "reserved suffix missing"
del self._metrics[metric_name + suffix]
del self._metrics[metric_name]
# requires lock def register_group(self, group: MetricGroup) -> None:
def _get_collection( for point in group.metric_points():
self, self.register(point)
path: Path,
metric_type: str,
group_definition: typing.Optional[MetricGroupDefinition] = None,
):
c = None
try:
c = self._metrics[path.name]
except KeyError:
pass
if not c is None:
if not isinstance(c, _MetricCollection):
c_parent = self._metrics[c]
assert isinstance(
c_parent, _MetricCollection), "nested metric groups not allowed"
if 0 == len(c_parent.metrics):
self._remove_collection(c)
else:
raise RuntimeError(
"Metric name {} reserved for group {}".format(path.name, c))
else:
if 0 == len(c.metrics):
self._remove_collection(path.name)
else:
c.check(path, metric_type, group_definition)
return c
c = _MetricCollection(metric_type, group_definition)
self._metrics[path.name] = c
return c
def register(self, path: Path, metric: typing.Union[MetricBase, MetricGroupBase], strong: bool = False):
with self._lock:
if metric in self._paths:
raise RuntimeError("metric already registered")
if isinstance(metric, MetricGroupBase):
group_definition = metric.group_definition
else:
group_definition = None
c = self._get_collection(
path, metric.metric_type, group_definition)
c.insert(path, metric)
self._paths[metric] = path
if strong:
# keep metric alive
self._strong_metrics.add(metric)
def collect(self):
metrics = []
with self._lock:
for (metric_name, c) in self._metrics.items():
data = [(k, m.get()) for (k, m) in c.metrics.items()]
metrics.append((metric_name, c.metric_type, c.help_text, data))
def collect(self) -> str:
from io import StringIO from io import StringIO
result = StringIO() result = StringIO()
for (metric_name, metric_type, help_text, data) in metrics:
if 0 == len(data): def escape_text(text: str) -> str:
continue return text.replace('\\', r'\\').replace('\n', r'\n')
if not help_text is None:
result.write("# HELP {} {}\n".format( with self._lock:
metric_name, help_text.replace('\\', r'\\').replace('\n', r'\n'))) families = list(self._families.values())
result.write('# TYPE {} {}\n'.format(metric_name, metric_type))
for (key, entry) in data: first = True
if isinstance(entry, MetricValue): for fam_instance in families:
if entry.timestamp is None: if first:
timestamp = '' first = False
else: else:
timestamp = ' {0:d}'.format(entry.timestamp) result.write("\n")
result.write("{} {} {}{}\n".format( family = fam_instance._family
metric_name, key, _floatToGoString(entry.value), timestamp)) if family.help:
result.write(f"# HELP {family.name} {escape_text(family.help)}\n")
if family.unit:
result.write(f"# UNIT {family.name} {family.unit}\n")
result.write(f"# TYPE {family.name} {family.TYPE}\n")
for key, value_set in fam_instance._get_value_sets():
if value_set.timestamp is None:
timestamp_s = ''
else: else:
for (subpath, subentry) in entry: timestamp_s = ' {0:d}'.format(value_set.timestamp)
if subentry.timestamp is None: for suffix, value in value_set.values:
timestamp = '' value_s = _floatToGoString(value)
else: result.write(f"{family.name}{suffix}{key} {value_s}{timestamp_s}\n")
timestamp = ' {0:d}'.format(subentry.timestamp)
result.write("{} {} {}{}\n".format(
subpath.name, subpath.labels.key, _floatToGoString(subentry.value), timestamp))
return result.getvalue() return result.getvalue()
GLOBAL_REGISTRY = Registry()

34
test.py
View File

@ -1,13 +1,27 @@
from prometheus_rus import Path, Counter, Gauge, Summary, GLOBAL_REGISTRY, NOW from prometheus_rus import CounterFamily, GaugeFamily, SummaryFamily, Registry
g1 = Gauge(value=20, path=Path("foobar_g1", {"server": "[host:xy]"}), help="foo help with bar") registry = Registry()
g2 = Gauge(path=Path("foobar_g2", {"server": "[host:xy]"}), help="foo help with bar")
gf1 = GaugeFamily(name="foobar_g1", help="foo help with bar")
g1 = gf1.create(value=20, labels=dict(server="[host:xy]"))
registry.register(g1)
g1.inc(10) g1.inc(10)
c = Counter(
path=Path("foobar", {"server": "[host:xy]"}), help="foo help with bar") gf2 = GaugeFamily(name="foobar_g2", help="foo help with bar")
g2 = gf2.create(labels=dict(server="[host:xy]"))
registry.register(g2)
cf = CounterFamily(name="foobar", help="foo help with bar")
c = cf.create(labels=dict(server="[host:xy]"))
registry.register(c)
c.set(1024.12374981723) c.set(1024.12374981723)
s = Summary(path=Path("m_sum_foo", {"tag": "sum"}), help="count on it!")
s.observe(1) sf = SummaryFamily(name="m_sum_foo", help="count on it!")
s.observe(2) s = sf.create(labels={"tag": "sum"})
s.observe(3, NOW) registry.register(s)
print(GLOBAL_REGISTRY.collect()) s.observe(1, timestamp=None)
s.observe(2, timestamp=None)
s.observe(3)
print(registry.collect())