|
15 | 15 | from .samples import Exemplar, Sample
|
16 | 16 | from .utils import floatToGoString, INF
|
17 | 17 |
|
| 18 | +import pandas as pd |
| 19 | + |
18 | 20 | T = TypeVar('T', bound='MetricWrapperBase')
|
19 | 21 | F = TypeVar("F", bound=Callable[..., Any])
|
20 | 22 |
|
@@ -714,3 +716,299 @@ def _child_samples(self) -> Iterable[Sample]:
|
714 | 716 | for i, s
|
715 | 717 | in enumerate(self._states)
|
716 | 718 | ]
|
| 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 | + |
0 commit comments