10000 initial commit with some tests and replaced _metrics with pandas Data… · engFelipeMonteiro/client_python@ea13dac · GitHub
[go: up one dir, main page]

Skip to content

Commit ea13dac

Browse files
initial commit with some tests and replaced _metrics with pandas DataFrame
Signed-off-by: Felipe Monteiro Jácome <fmj1988@gmail.com>
1 parent ecb5dd9 commit ea13dac

File tree

2 files changed

+332
-0
lines changed

2 files changed

+332
-0
lines changed

prometheus_client/metrics.py

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from .samples import Exemplar, Sample
1616
from .utils import floatToGoString, INF
1717

18+
import pandas as pd
19+
1820
T = TypeVar('T', bound='MetricWrapperBase')
1921
F = TypeVar("F", bound=Callable[..., Any])
2022

@@ -714,3 +716,299 @@ def _child_samples(self) -> Iterable[Sample]:
714716
for i, s
715717
in enumerate(self._states)
716718
]
719+
720+
721+
class MetricPandasWrapperBase:
722+
_type: Optional[str] = None
723+
_reserved_labelnames: Sequence[str] = ()
724+
725+
def _is_observable(self):
726+
# Whether this metric is observable, i.e.
727+
# * a metric without label names and values, or
728+
# * the child of a labelled metric.
729+
return not self._labelnames or (self._labelnames and self._labelvalues)
730+
731+
def _raise_if_not_observable(self):
732+
# Functions that mutate the state of the metric, for example incrementing
733+
# a counter, will fail if the metric is not observable, because only if a
734+
# metric is observable will the value be initialized.
735+
if not self._is_observable():
736+
raise ValueError('%s metric is missing label values' % str(self._type))
737+
738+
def _is_parent(self):
739+
return self._labelnames and not self._labelvalues
740+
741+
def _get_metric(self):
742+
return Metric(self._name, self._documentation, self._type, self._unit)
743+
744+
def describe(self):
745+
return [self._get_metric()]
746+
747+
def collect(self):
748+
metric = self._get_metric()
749+
for suffix, labels, value, timestamp, exemplar in self._samples():
750+
metric.add_sample(self._name + suffix, labels, value, timestamp, exemplar)
751+
return [metric]
752+
753+
def __str__(self):
754+
return f"{self._type}:{self._name}"
755+
756+
def __repr__(self):
757+
metric_type = type(self)
758+
return f"{metric_type.__module__}.{metric_type.__name__}({self._name})"
759+
760+
def __init__(self: T,
761+
name: str,
762+
documentation: str,
763+
labelnames: Iterable[str] = (),
764+
namespace: str = '',
765+
subsystem: str = '',
766+
unit: str = '',
767+
registry: Optional[CollectorRegistry] = REGISTRY,
768+
_labelvalues: Optional[Sequence[str]] = None,
769+
) -> None:
770+
self._name = _build_full_name(self._type, name, namespace, subsystem, unit)
771+
self._labelnames = _validate_labelnames(self, labelnames)
772+
self._labelvalues = tuple(_labelvalues or ())
773+
self._kwargs: Dict[str, Any] = {}
774+
self._documentation = documentation
775+
self._unit = unit
776+
777+
if not METRIC_NAME_RE.match(self._name):
778+
raise ValueError('Invalid metric name: ' + self._name)
779+
780+
if self._is_parent():
781+
# Prepare the fields needed for child metrics.
782+
self._lock = Lock()< F438 /div>
783+
self._metrics = pd.DataFrame(columns=labelnames)
784+
785+
if self._is_observable():
786+
self._metric_init()
787+
788+
if not self._labelvalues:
789+
# Register the multi-wrapper parent metric, or if a label-less metric, the whole shebang.
790+
if registry:
791+
registry.register(self)
792+
793+
def labels(self: T, *labelvalues: Any, **labelkwargs: Any) -> T:
794+
"""Return the child for the given labelset.
795+
796+
All metrics can have labels, allowing grouping of related time series.
797+
Taking a counter as an example:
798+
799+
from prometheus_client import Counter
800+
801+
c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
802+
c.labels('get', '/').inc()
803+
c.labels('post', '/submit').inc()
804+
805+
Labels can also be provided as keyword arguments:
806+
807+
from prometheus_client import Counter
808+
809+
c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
810+
c.labels(method='get', endpoint='/').inc()
811+
c.labels(method='post', endpoint='/submit').inc()
812+
813+
See the best practices on [naming](http://prometheus.io/docs/practices/naming/)
814+
and [labels](http://prometheus.io/docs/practices/instrumentation/#use-labels).
815+
"""
816+
if not self._labelnames:
817+
raise ValueError('No label names were set when constructing %s' % self)
818+
819+
if self._labelvalues:
820+
raise ValueError('{} already has labels set ({}); can not chain calls to .labels()'.format(
821+
self,
822+
dict(zip(self._labelnames, self._labelvalues))
823+
))
824+
10000 825+
if labelvalues and labelkwargs:
826+
raise ValueError("Can't pass both *args and **kwargs")
827+
828+
if labelkwargs:
829+
if sorted(labelkwargs) != sorted(self._labelnames):
830+
raise ValueError('Incorrect label names')
831+
labelvalues = tuple(str(labelkwargs[l]) for l in self._labelnames)
832+
else:
833+
if len(labelvalues) != len(self._labelnames):
834+
raise ValueError('Incorrect label count')
835+
labelvalues = tuple(str(l) for l in labelvalues)
836+
with self._lock:
837+
if labelvalues not in self._metrics:
838+
self._metrics[labelvalues] = self.__class__(
839+
self._name,
840+
documentation=self._documentation,
841+
labelnames=self._labelnames,
842+
unit=self._unit,
843+
_labelvalues=labelvalues,
844+
**self._kwargs
845+
)
846+
return self._metrics[labelvalues]
847+
848+
def remove(self, *labelvalues):
849+
if not self._labelnames:
850+
raise ValueError('No label names were set when constructing %s' % self)
851+
852+
"""Remove the given labelset from the metric."""
853+
if len(labelvalues) != len(self._labelnames):
854+
raise ValueError('Incorrect label count (expected %d, got %s)' % (len(self._labelnames), labelvalues))
855+
labelvalues = tuple(str(l) for l in labelvalues)
856+
with self._lock:
857+
del self._metrics[labelvalues]
858+
859+
def clear(self) -> None:
860+
"""Remove all labelsets from the metric"""
861+
with self._lock:
862+
self._metrics = {}
863+
864+
def _samples(self) -> Iterable[Sample]:
865+
if self._is_parent():
866+
return self._multi_samples()
867+
else:
868+
return self._child_samples()
869+
870+
def _multi_samples(self) -> Iterable[Sample]:
871+
with self._lock:
872+
metrics = self._metrics.copy()
873+
for labels, metric in metrics.items():
874+
series_labels = list(zip(self._labelnames, labels))
875+
for suffix, sample_labels, value, timestamp, exemplar in metric._samples():
876+
yield Sample(suffix, dict(series_labels + list(sample_labels.items())), value, timestamp, exemplar)
877+
878+
def _child_samples(self) -> Iterable[Sample]: # pragma: no cover
879+
raise NotImplementedError('_child_samples() must be implemented by %r' % self)
880+
881+
def _metric_init(self): # pragma: no cover
882+
"""
883+
Initialize the metric object as a child, i.e. when it has labels (if any) set.
884+
885+
This is factored as a separate function to allow for deferred initialization.
886+
"""
887+
raise NotImplementedError('_metric_init() must be implemented by %r' % self)
888+
889+
890+
class GaugePandas(MetricPandasWrapperBase):
891+
"""Gauge metric, to report instantaneous values.
892+
893+
Examples of Gauges include:
894+
- Inprogress requests
895+
- Number of items in a queue
896+
- Free memory
897+
- Total memory
898+
- Temperature
899+
900+
Gauges can go both up and down.
901+
902+
from prometheus_client import Gauge
903+
904+
g = Gauge('my_inprogress_requests', 'Description of gauge')
905+
g.inc() # Increment by 1
906+
g.dec(10) # Decrement by given value
907+
g.set(4.2) # Set to a given value
908+
909+
There are utilities for common use cases:
910+
911+
g.set_to_current_time() # Set to current unixtime
912+
913+
# Increment when entered, decrement when exited.
914+
@g.track_inprogress()
915+
def f():
916+
pass
917+
918+
with g.track_inprogress():
919+
pass
920+
921+
A Gauge can also take its value from a callback:
922+
923+
d = Gauge('data_objects', 'Number of objects')
924+
my_dict = {}
925+
d.set_function(lambda: len(my_dict))
926+
"""
927+
_type = 'gauge'
928+
_MULTIPROC_MODES = frozenset(('min', 'max', 'livesum', 'liveall', 'all'))
929+
930+
def __init__(self,
931+
name: str,
932+
documentation: str,
933+
labelnames: Iterable[str] = (),
934+
namespace: str = '',
935+
subsystem: str = '',
936+
unit: str = '',
937+
registry: Optional[CollectorRegistry] = REGISTRY,
938+
_labelvalues: Optional[Sequence[str]] = None,
939+
multiprocess_mode: str = 'all',
940+
):
941+
self._multiprocess_mode = multiprocess_mode
942+
if multiprocess_mode not in self._MULTIPROC_MODES:
943+
raise ValueError('Invalid multiprocess mode: ' + multiprocess_mode)
944+
super().__init__(
945+
name=name,
946+
documentation=documentation,
947+
labelnames=labelnames,
948+
namespace=namespace,
949+
subsystem=subsystem,
950+
unit=unit,
951+
registry=registry,
952+
_labelvalues=_labelvalues,
953+
)
954+
self._kwargs['multiprocess_mode'] = self._multiprocess_mode
955+
956+
def _metric_init(self) -> None:
957+
self._value = values.ValueClass(
958+
self._type, self._name, self._name, self._labelnames, self._labelvalues,
959+
multiprocess_mode=self._multiprocess_mode
960+
)
961+
962+
def inc(self, amount: float = 1) -> None:
963+
"""Increment gauge by the given amount."""
964+
self._raise_if_not_observable()
965+
self._value.inc(amount)
966+
967+
def dec(self, amount: float = 1) -> None:
968+
"""Decrement gauge by the given amount."""
969+
self._raise_if_not_observable()
970+
self._value.inc(-amount)
971+
972+
def set(self, value: float) -> None:
973+
"""Set gauge to the given value."""
974+
self._raise_if_not_observable()
975+
self._value.set(float(value))
976+
977+
def set_to_current_time(self) -> None:
978+
"""Set gauge to the current unixtime."""
979+
self.set(time.time())
980+
981+
def track_inprogress(self) -> InprogressTracker:
982+
"""Track inprogress blocks of code or functions.
983+
984+
Can be used as a function decorator or context manager.
985+
Increments the gauge when the code is entered,
986+
and decrements when it is exited.
987+
"""
988+
self._raise_if_not_observable()
989+
return InprogressTracker(self)
990+
991+
def time(self) -> Timer:
992+
"""Time a block of code or function, and set the duration in seconds.
993+
994+
Can be used as a function decorator or context manager.
995+
"""
996+
return Timer(self, 'set')
997+
998+
def set_function(self, f: Callable[[], float]) -> None:
999+
"""Call the provided function to return the Gauge value.
1000+
1001+
The function must return a float, and may be called from
1002+
multiple threads. All other methods of the Gauge become NOOPs.
1003+
"""
1004+
1005+
self._raise_if_not_observable()
1006+
1007+
def samples(_: Gauge) -> Iterable[Sample]:
1008+
return (Sample('', {}, float(f()), None, None),)
1009+
1010+
self._child_samples = types.MethodType(samples, self) # type: ignore
1011+
1012+
def _child_samples(self) -> Iterable[Sample]:
1013+
return (Sample('', {}, self._value.get(), None, None),)
1014+

tests/test_registry.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from prometheus_client.registry import CollectorRegistry
2+
from prometheus_client.metrics import Gauge, GaugePandas
3+
import pandas as pd
4+
5+
def test_collector_registry_init():
6+
registry = CollectorRegistry()
7+
assert registry._collector_to_names == {}
8+
assert registry._names_to_collectors == {}
9+
assert registry._auto_describe == False
10+
assert str(type(registry._lock)) == "<class '_thread.lock'>"
11+
assert registry._target_info == None
12+
13+
def test_collector_registry_gauge():
14+
registry = CollectorRegistry()
15+
g = Gauge('raid_status', '1 if raid array is okay', registry=registry)
16+
g.set(1)
17+
18+
assert registry._names_to_collectors['raid_status'] == g
19+
assert registry._names_to_collectors['raid_status']._documentation == '1 if raid array is okay'
20+
assert '_metrics' not in vars(registry._names_to_collectors['raid_status'])
21+
22+
G = Gauge('raid_status2', '1 if raid array is okay', ['label1'], registry=registry)
23+
#G.labels('a').set(10)
24+
#G.labels('b').set(11)
25 6BA7 +
#G.labels('c').set(12)
26+
#G.labels('c').set(13)
27+
28+
assert registry._names_to_collectors['raid_status2']._labelnames == ('label1',)
29+
'_metrics' in vars(registry._names_to_collectors['raid_status2'])
30+
31+
registry2 = CollectorRegistry()
32+
GP = GaugePandas('raid_status2', '1 if raid array is okay', ['label1'], registry=registry2)
33+
assert type(GP._metrics) == pd.core.frame.DataFrame
34+
import pdb; pdb.set_trace()

0 commit comments

Comments
 (0)
0