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:
6 # http://www.apache.org/licenses/LICENSE-2.0
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.
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.
22 from collections import namedtuple
23 from threading import Lock
30 A value storage protected by a mutex.
34 Initialize value to default and create a lock.
38 self._timestamp = None
40 def inc(self, amount):
42 Increment value by amount under mutex.
43 Add a timestamp of capturing value.
45 :param amount: Amount of increment.
46 :type amount: int or float
50 self._timestamp = time()
54 Set to a specific value under mutex.
55 Add a timestamp of capturing value.
57 :param value: Amount of increment.
58 :type value: int or float
62 self._timestamp = time()
66 Get a value under mutex.
68 :returns: Stored value.
74 def get_timestamp(self):
76 Get a timestamp under mutex.
78 :returns: Stored timestamp.
82 return self._timestamp
87 A single metric parent and its samples.
89 def __init__(self, name, documentation, typ):
91 Initialize class and do basic sanitize.
93 :param name: Full metric name.
94 :param documentation: Metric HELP string.
95 :param typ: Metric type [counter|gauge|info].
97 :type documentation: str
100 self.metric_types = (
101 u"counter", u"gauge", u"info"
103 self.metric_sample = namedtuple(
104 u"Sample", [u"name", u"labels", u"value", u"timestamp"]
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}!")
113 self.documentation = documentation
117 def add_sample(self, name, labels, value, timestamp):
119 Add a sample (entry) to the metric.
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.
127 :type value: int or float
128 :type timestamp: float
131 self.metric_sample(name, labels, value, timestamp)
134 def __eq__(self, other):
136 Check equality of added metric.
138 :param other: Metric to compare.
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)
149 Represantation as a string for a debug print.
152 f"Metric({self.name}, "
153 f"{self.documentation}, "
161 Abstract class for Metric implementation.
166 self, name, documentation, labelnames=(), namespace="",
167 subsystem="", labelvalues=None,
170 Metric initialization.
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.
179 :type documentation: str
180 :type labelnames: list
183 :type labelvalues: list
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
190 if self._is_parent():
194 if self._is_observable():
198 def validate_name(name, namespace, subsystem):
200 Construct metric full name and validate naming convention.
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).
208 :returns: Metric full name.
210 :rasies ValueError: If name does not conform with naming conventions.
213 full_name += f"{namespace}_" if namespace else u""
214 full_name += f"{subsystem}_" if subsystem else u""
217 if not re.compile(r"^[a-zA-Z_:][a-zA-Z0-9_:]*$").match(full_name):
219 f"Invalid metric name: {full_name}!"
224 def validate_labelnames(labelnames):
226 Create label tuple and validate naming convention.
228 :param labelnames: Metric label list.
229 :type labelnames: list
230 :returns: Label names.
232 :rasies ValueError: If name does not conform with naming conventions.
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}!")
242 def _is_observable(self):
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.
251 return not self._labelnames or (self._labelnames and self._labelvalues)
253 def _is_parent(self):
255 Check whether metric is parent, i.e.
256 * a metric with label names but not its values.
261 return self._labelnames and not self._labelvalues
263 def _get_metric(self):
265 Returns metric that will handle samples.
267 :returns: Metric object.
270 return Metric(self._name, self._documentation, self._type)
274 Returns metric that will handle samples.
276 :returns: List of metric objects.
279 return [self._get_metric()]
283 Returns metric with samples.
285 :returns: List with metric object.
288 metric = self._get_metric()
289 for suffix, labels, value, timestamp in self.samples():
290 metric.add_sample(self._name + suffix, labels, value, timestamp)
293 def labels(self, *labelvalues, **labelkwargs):
295 Return the child for the given labelset.
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.
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.
308 if not self._labelnames:
310 f"No label names were set when constructing {self}!"
313 if self._labelvalues:
315 f"{self} already has labels set; can not chain .labels() calls!"
318 if labelvalues and labelkwargs:
320 u"Can't pass both *args and **kwargs!"
324 if sorted(labelkwargs) != sorted(self._labelnames):
325 raise ValueError(u"Incorrect label names!")
326 labelvalues = tuple(labelkwargs[l] for l in self._labelnames)
328 if len(labelvalues) != len(self._labelnames):
329 raise ValueError(u"Incorrect label count!")
330 labelvalues = tuple(l for l in labelvalues)
332 if labelvalues not in self._metrics:
333 self._metrics[labelvalues] = self.__class__(
335 documentation=self._documentation,
336 labelnames=self._labelnames,
337 labelvalues=labelvalues
339 return self._metrics[labelvalues]
343 Returns samples wheter an object is parent or child.
345 :returns: List of Metric objects with values.
348 if self._is_parent():
349 return self._multi_samples()
350 return self._child_samples()
352 def _multi_samples(self):
354 Returns parent and its childs with its values.
356 :returns: List of Metric objects with values.
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():
365 suffix, dict(series_labels + list(sample_labels.items())),
369 def _child_samples(self):
371 Returns child with its values. Should be implemented by child class.
373 :raises NotImplementedError: If implementation in not in subclass.
375 raise NotImplementedError(
376 f"_child_samples() must be implemented by {self}!"
379 def _metric_init(self):
381 Initialize the metric object as a child.
383 :raises NotImplementedError: If implementation in not in subclass.
385 raise NotImplementedError(
386 f"_metric_init() must be implemented by {self}!"
391 String for a debug print.
393 return f"{self._type}:{self._name}"
397 Represantation as a string for a debug print.
399 metric_type = type(self)
400 return f"{metric_type.__module__}.{metric_type.__name__}({self._name})"
403 class Counter(MetricBase):
405 A Counter tracks counts of events or running totals.
418 Initialize the Counter metric object.
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.
427 :type documentation: str
428 :type labelnames: list
431 :type labelvalues: list
433 super(Counter, self).__init__(
435 documentation=documentation,
436 labelnames=labelnames,
439 labelvalues=labelvalues,
442 def _metric_init(self):
444 Initialize counter value.
446 self._value = Value()
448 def inc(self, amount=1):
450 Increment counter by the given amount.
452 :param amount: Amount to increment.
453 :type amount: int or float
454 :raises ValueError: If amout is not positive.
458 u"Counters can only be incremented by non-negative amounts."
460 self._value.inc(amount)
462 def _child_samples(self):
464 Returns list of child samples.
466 :returns: List of child samples.
469 return ((u"", {}, self._value.get(), self._value.get_timestamp()),)
472 class Gauge(MetricBase):
474 Gauge metric, to report instantaneous values.
487 Initialize the Gauge metric object.
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.
496 :type documentation: str
497 :type labelnames: list
500 :type labelvalues: list
502 super(Gauge, self).__init__(
504 documentation=documentation,
505 labelnames=labelnames,
508 labelvalues=labelvalues,
511 def _metric_init(self):
513 Initialize gauge value.
515 self._value = Value()
517 def inc(self, amount=1):
519 Increment gauge by the given amount.
521 :param amount: Amount to increment.
522 :type amount: int or float
524 self._value.inc(amount)
526 def dec(self, amount=1):
528 Decrement gauge by the given amount.
530 :param amount: Amount to decrement.
531 :type amount: int or float
533 self._value.inc(-amount)
535 def set(self, value):
537 Set gauge to the given value.
539 :param amount: Value to set.
540 :type amount: int or float
542 self._value.set(float(value))
544 def _child_samples(self):
546 Returns list of child samples.
548 :returns: List of child samples.
551 return ((u"", {}, self._value.get(), self._value.get_timestamp()),)
554 class Info(MetricBase):
556 Info metric, key-value pairs.
569 Initialize the Info metric object.
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.
578 :type documentation: str
579 :type labelnames: list
582 :type labelvalues: list
584 super(Info, self).__init__(
586 documentation=documentation,
587 labelnames=labelnames,
590 labelvalues=labelvalues,
593 def _metric_init(self):
595 Initialize gauge value and time it was created.
597 self._labelname_set = set(self._labelnames)
601 def info(self, value):
603 Set info to the given value.
605 :param amount: Value to set.
606 :type amount: int or float
607 :raises ValueError: If lables are overlapping.
609 if self._labelname_set.intersection(value.keys()):
611 u"Overlapping labels for Info metric, "
612 f"metric: {self._labelnames} child: {value}!"
615 self._value = dict(value)
617 def _child_samples(self):
619 Returns list of child samples.
621 :returns: List of child samples.
625 return ((u"_info", self._value, 1.0, time()),)