281760183b1022c9b42be6c8d87c04373e5c8421
[csit.git] / resources / tools / telemetry / metrics.py
1 # Copyright (c) 2021 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Metric library.
15
16 Time measurements are done by time.time().
17 Although time.time() is susceptible to big (or even negative) jumps
18 when a system is badly synchronized, it is still better
19 than time.monotonic(), as that value has no relation to epoch time.
20 """
21
22 from collections import namedtuple
23 from threading import Lock
24 from time import time
25 import re
26
27
28 class Value:
29     """
30     A value storage protected by a mutex.
31     """
32     def __init__(self):
33         """
34         Initialize value to default and create a lock.
35         """
36         self._value = 0.0
37         self._lock = Lock()
38         self._timestamp = None
39
40     def inc(self, amount):
41         """
42         Increment value by amount under mutex.
43         Add a timestamp of capturing value.
44
45         :param amount: Amount of increment.
46         :type amount: int or float
47         """
48         with self._lock:
49             self._value += amount
50             self._timestamp = time()
51
52     def set(self, value):
53         """
54         Set to a specific value under mutex.
55         Add a timestamp of capturing value.
56
57         :param value: Amount of increment.
58         :type value: int or float
59         """
60         with self._lock:
61             self._value = value
62             self._timestamp = time()
63
64     def get(self):
65         """
66         Get a value under mutex.
67
68         :returns: Stored value.
69         :rtype: int or float
70         """
71         with self._lock:
72             return self._value
73
74     def get_timestamp(self):
75         """
76         Get a timestamp under mutex.
77
78         :returns: Stored timestamp.
79         :rtype: str
80         """
81         with self._lock:
82             return self._timestamp
83
84
85 class Metric:
86     """
87     A single metric parent and its samples.
88     """
89     def __init__(self, name, documentation, typ):
90         """
91         Initialize class and do basic sanitize.
92
93         :param name: Full metric name.
94         :param documentation: Metric HELP string.
95         :param typ: Metric type [counter|gauge|info].
96         :type name: str
97         :type documentation: str
98         :type typ: str
99         """
100         self.metric_types = (
101             u"counter", u"gauge", u"info"
102         )
103         self.metric_sample = namedtuple(
104             u"Sample", [u"name", u"labels", u"value", u"timestamp"]
105         )
106
107         if not re.compile(r"^[a-zA-Z_:][a-zA-Z0-9_:]*$").match(name):
108             raise ValueError(f"Invalid metric name: {name}!")
109         if typ not in self.metric_types:
110             raise ValueError(f"Invalid metric type: {typ}!")
111
112         self.name = name
113         self.documentation = documentation
114         self.type = typ
115         self.samples = []
116
117     def add_sample(self, name, labels, value, timestamp):
118         """
119         Add a sample (entry) to the metric.
120
121         :param name: Full metric name.
122         :param labels: Metric labels.
123         :param value: Metric value.
124         :param timestamp: Timestamp. Default to be when accessed.
125         :type name: str
126         :type lables: tuple
127         :type value: int or float
128         :type timestamp: float
129         """
130         self.samples.append(
131             self.metric_sample(name, labels, value, timestamp)
132         )
133
134     def __eq__(self, other):
135         """
136         Check equality of added metric.
137
138         :param other: Metric to compare.
139         :type other: Metric
140         """
141         return (isinstance(other, Metric)
142                 and self.name == other.name
143                 and self.documentation == other.documentation
144                 and self.type == other.type
145                 and self.samples == other.samples)
146
147     def __repr__(self):
148         """
149         Represantation as a string for a debug print.
150         """
151         return (
152             f"Metric({self.name}, "
153             f"{self.documentation}, "
154             f"{self.type}, "
155             f"{self.samples})"
156         )
157
158
159 class MetricBase:
160     """
161     Abstract class for Metric implementation.
162     """
163     _type = None
164
165     def __init__(
166             self, name, documentation, labelnames=(), namespace="",
167             subsystem="", labelvalues=None,
168         ):
169         """
170         Metric initialization.
171
172         :param name: Metric name.
173         :param documentation: Metric HELP string.
174         :param labelnames: Metric label list.
175         :param namespace: Metric namespace (will be added as prefix).
176         :param subsystem: Metric susbsystem (will be added as prefix).
177         :param labelvalues: Metric label values.
178         :type name: str
179         :type documentation: str
180         :type labelnames: list
181         :type namespace: str
182         :type subsystem: str
183         :type labelvalues: list
184         """
185         self._name = self.validate_name(name, namespace, subsystem)
186         self._labelnames = self.validate_labelnames(labelnames)
187         self._labelvalues = tuple(labelvalues or ())
188         self._documentation = documentation
189
190         if self._is_parent():
191             self._lock = Lock()
192             self._metrics = {}
193
194         if self._is_observable():
195             self._metric_init()
196
197     @staticmethod
198     def validate_name(name, namespace, subsystem):
199         """
200         Construct metric full name and validate naming convention.
201
202         :param name: Metric name.
203         :param namespace: Metric namespace (will be added as prefix).
204         :param subsystem: Metric susbsystem (will be added as prefix).
205         :type name: str
206         :type namespace: str
207         :type subsystem: str
208         :returns: Metric full name.
209         :rtype: str
210         :rasies ValueError: If name does not conform with naming conventions.
211         """
212         full_name = u""
213         full_name += f"{namespace}_" if namespace else u""
214         full_name += f"{subsystem}_" if subsystem else u""
215         full_name += name
216
217         if not re.compile(r"^[a-zA-Z_:][a-zA-Z0-9_:]*$").match(full_name):
218             raise ValueError(
219                 f"Invalid metric name: {full_name}!"
220             )
221         return full_name
222
223     @staticmethod
224     def validate_labelnames(labelnames):
225         """
226         Create label tuple and validate naming convention.
227
228         :param labelnames: Metric label list.
229         :type labelnames: list
230         :returns: Label names.
231         :rtype: tuple
232         :rasies ValueError: If name does not conform with naming conventions.
233         """
234         labelnames = tuple(labelnames)
235         for label in labelnames:
236             if not re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$").match(label):
237                 raise ValueError(f"Invalid label metric name: {label}!")
238             if re.compile(r"^__.*$").match(label):
239                 raise ValueError(f"Reserved label metric name: {label}!")
240         return labelnames
241
242     def _is_observable(self):
243         """
244         Check whether this metric is observable, i.e.
245         * a metric without label names and values, or
246         * the child of a labelled metric.
247
248         :return: Observable
249         :rtype: bool
250         """
251         return not self._labelnames or (self._labelnames and self._labelvalues)
252
253     def _is_parent(self):
254         """
255         Check whether metric is parent, i.e.
256         * a metric with label names but not its values.
257
258         :return: Parent
259         :rtype: bool
260         """
261         return self._labelnames and not self._labelvalues
262
263     def _get_metric(self):
264         """
265         Returns metric that will handle samples.
266
267         :returns: Metric object.
268         :rtype: Metric
269         """
270         return Metric(self._name, self._documentation, self._type)
271
272     def describe(self):
273         """
274         Returns metric that will handle samples.
275
276         :returns: List of metric objects.
277         :rtype: list
278         """
279         return [self._get_metric()]
280
281     def collect(self):
282         """
283         Returns metric with samples.
284
285         :returns: List with metric object.
286         :rtype: list
287         """
288         metric = self._get_metric()
289         for suffix, labels, value, timestamp in self.samples():
290             metric.add_sample(self._name + suffix, labels, value, timestamp)
291         return [metric]
292
293     def labels(self, *labelvalues, **labelkwargs):
294         """
295         Return the child for the given labelset.
296
297         :param labelvalues: Label values.
298         :param labelkwargs: Dictionary with label names and values.
299         :type labelvalues: list
300         :type labelkwargs: dict
301         :returns: Metric with labels and values.
302         :rtype: Metric
303         :raises ValueError: If labels were not initialized.
304         :raises ValueError: If labels are already set (chaining).
305         :raises ValueError: If both parameters are passed.
306         :raises ValueError: If label values are not matching label names.
307         """
308         if not self._labelnames:
309             raise ValueError(
310                 f"No label names were set when constructing {self}!"
311             )
312
313         if self._labelvalues:
314             raise ValueError(
315                 f"{self} already has labels set; can not chain .labels() calls!"
316             )
317
318         if labelvalues and labelkwargs:
319             raise ValueError(
320                 u"Can't pass both *args and **kwargs!"
321             )
322
323         if labelkwargs:
324             if sorted(labelkwargs) != sorted(self._labelnames):
325                 raise ValueError(u"Incorrect label names!")
326             labelvalues = tuple(labelkwargs[l] for l in self._labelnames)
327         else:
328             if len(labelvalues) != len(self._labelnames):
329                 raise ValueError(u"Incorrect label count!")
330             labelvalues = tuple(l for l in labelvalues)
331         with self._lock:
332             if labelvalues not in self._metrics:
333                 self._metrics[labelvalues] = self.__class__(
334                     self._name,
335                     documentation=self._documentation,
336                     labelnames=self._labelnames,
337                     labelvalues=labelvalues
338                 )
339             return self._metrics[labelvalues]
340
341     def samples(self):
342         """
343         Returns samples wheter an object is parent or child.
344
345         :returns: List of Metric objects with values.
346         :rtype: list
347         """
348         if self._is_parent():
349             return self._multi_samples()
350         return self._child_samples()
351
352     def _multi_samples(self):
353         """
354         Returns parent and its childs with its values.
355
356         :returns: List of Metric objects with values.
357         :rtype: list
358         """
359         with self._lock:
360             metrics = self._metrics.copy()
361         for labels, metric in metrics.items():
362             series_labels = list(zip(self._labelnames, labels))
363             for suffix, sample_labels, value, timestamp in metric.samples():
364                 yield (
365                     suffix, dict(series_labels + list(sample_labels.items())),
366                     value, timestamp
367                 )
368
369     def _child_samples(self):
370         """
371         Returns child with its values. Should be implemented by child class.
372
373         :raises NotImplementedError: If implementation in not in subclass.
374         """
375         raise NotImplementedError(
376             f"_child_samples() must be implemented by {self}!"
377         )
378
379     def _metric_init(self):
380         """
381         Initialize the metric object as a child.
382
383         :raises NotImplementedError: If implementation in not in subclass.
384         """
385         raise NotImplementedError(
386             f"_metric_init() must be implemented by {self}!"
387         )
388
389     def __str__(self):
390         """
391         String for a debug print.
392         """
393         return f"{self._type}:{self._name}"
394
395     def __repr__(self):
396         """
397         Represantation as a string for a debug print.
398         """
399         metric_type = type(self)
400         return f"{metric_type.__module__}.{metric_type.__name__}({self._name})"
401
402
403 class Counter(MetricBase):
404     """
405     A Counter tracks counts of events or running totals.
406     """
407     _type = u"counter"
408
409     def __init__(self,
410                  name,
411                  documentation,
412                  labelnames=(),
413                  namespace=u"",
414                  subsystem=u"",
415                  labelvalues=None
416                  ):
417         """
418         Initialize the Counter metric object.
419
420         :param name: Metric name.
421         :param documentation: Metric HELP string.
422         :param labelnames: Metric label list.
423         :param namespace: Metric namespace (will be added as prefix).
424         :param subsystem: Metric susbsystem (will be added as prefix).
425         :param labelvalues: Metric label values.
426         :type name: str
427         :type documentation: str
428         :type labelnames: list
429         :type namespace: str
430         :type subsystem: str
431         :type labelvalues: list
432         """
433         super(Counter, self).__init__(
434             name=name,
435             documentation=documentation,
436             labelnames=labelnames,
437             namespace=namespace,
438             subsystem=subsystem,
439             labelvalues=labelvalues,
440         )
441
442     def _metric_init(self):
443         """
444         Initialize counter value.
445         """
446         self._value = Value()
447
448     def inc(self, amount=1):
449         """
450         Increment counter by the given amount.
451
452         :param amount: Amount to increment.
453         :type amount: int or float
454         :raises ValueError: If amout is not positive.
455         """
456         if amount < 0:
457             raise ValueError(
458                 u"Counters can only be incremented by non-negative amounts."
459             )
460         self._value.inc(amount)
461
462     def _child_samples(self):
463         """
464         Returns list of child samples.
465
466         :returns: List of child samples.
467         :rtype: tuple
468         """
469         return ((u"", {}, self._value.get(), self._value.get_timestamp()),)
470
471
472 class Gauge(MetricBase):
473     """
474     Gauge metric, to report instantaneous values.
475     """
476     _type = u"gauge"
477
478     def __init__(self,
479                  name,
480                  documentation,
481                  labelnames=(),
482                  namespace=u"",
483                  subsystem=u"",
484                  labelvalues=None
485                  ):
486         """
487         Initialize the Gauge metric object.
488
489         :param name: Metric name.
490         :param documentation: Metric HELP string.
491         :param labelnames: Metric label list.
492         :param namespace: Metric namespace (will be added as prefix).
493         :param subsystem: Metric susbsystem (will be added as prefix).
494         :param labelvalues: Metric label values.
495         :type name: str
496         :type documentation: str
497         :type labelnames: list
498         :type namespace: str
499         :type subsystem: str
500         :type labelvalues: list
501         """
502         super(Gauge, self).__init__(
503             name=name,
504             documentation=documentation,
505             labelnames=labelnames,
506             namespace=namespace,
507             subsystem=subsystem,
508             labelvalues=labelvalues,
509         )
510
511     def _metric_init(self):
512         """
513         Initialize gauge value.
514         """
515         self._value = Value()
516
517     def inc(self, amount=1):
518         """
519         Increment gauge by the given amount.
520
521         :param amount: Amount to increment.
522         :type amount: int or float
523         """
524         self._value.inc(amount)
525
526     def dec(self, amount=1):
527         """
528         Decrement gauge by the given amount.
529
530         :param amount: Amount to decrement.
531         :type amount: int or float
532         """
533         self._value.inc(-amount)
534
535     def set(self, value):
536         """
537         Set gauge to the given value.
538
539         :param amount: Value to set.
540         :type amount: int or float
541         """
542         self._value.set(float(value))
543
544     def _child_samples(self):
545         """
546         Returns list of child samples.
547
548         :returns: List of child samples.
549         :rtype: tuple
550         """
551         return ((u"", {}, self._value.get(), self._value.get_timestamp()),)
552
553
554 class Info(MetricBase):
555     """
556     Info metric, key-value pairs.
557     """
558     _type = u"info"
559
560     def __init__(self,
561                  name,
562                  documentation,
563                  labelnames=(),
564                  namespace=u"",
565                  subsystem=u"",
566                  labelvalues=None
567                  ):
568         """
569         Initialize the Info metric object.
570
571         :param name: Metric name.
572         :param documentation: Metric HELP string.
573         :param labelnames: Metric label list.
574         :param namespace: Metric namespace (will be added as prefix).
575         :param subsystem: Metric susbsystem (will be added as prefix).
576         :param labelvalues: Metric label values.
577         :type name: str
578         :type documentation: str
579         :type labelnames: list
580         :type namespace: str
581         :type subsystem: str
582         :type labelvalues: list
583         """
584         super(Info, self).__init__(
585             name=name,
586             documentation=documentation,
587             labelnames=labelnames,
588             namespace=namespace,
589             subsystem=subsystem,
590             labelvalues=labelvalues,
591         )
592
593     def _metric_init(self):
594         """
595         Initialize gauge value and time it was created.
596         """
597         self._labelname_set = set(self._labelnames)
598         self._lock = Lock()
599         self._value = {}
600
601     def info(self, value):
602         """
603         Set info to the given value.
604
605         :param amount: Value to set.
606         :type amount: int or float
607         :raises ValueError: If lables are overlapping.
608         """
609         if self._labelname_set.intersection(value.keys()):
610             raise ValueError(
611                 u"Overlapping labels for Info metric, "
612                 f"metric: {self._labelnames} child: {value}!"
613             )
614         with self._lock:
615             self._value = dict(value)
616
617     def _child_samples(self):
618         """
619         Returns list of child samples.
620
621         :returns: List of child samples.
622         :rtype: tuple
623         """
624         with self._lock:
625             return ((u"_info", self._value, 1.0, time()),)