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 from collections import namedtuple
17 from threading import Lock
18 from time import monotonic
24 A value storage protected by a mutex.
28 Initialize value to default and create a lock.
32 self._timestamp = None
34 def inc(self, amount):
36 Increment value by amount under mutex.
37 Add a timestamp of capturing value.
39 :param amount: Amount of increment.
40 :type amount: int or float
44 self._timestamp = monotonic()
48 Set to a specific value under mutex.
49 Add a timestamp of capturing value.
51 :param value: Amount of increment.
52 :type value: int or float
56 self._timestamp = monotonic()
60 Get a value under mutex.
62 :returns: Stored value.
68 def get_timestamp(self):
70 Get a timestamp under mutex.
72 :returns: Stored timestamp.
76 return self._timestamp
81 A single metric parent and its samples.
83 def __init__(self, name, documentation, typ):
85 Initialize class and do basic sanitize.
87 :param name: Full metric name.
88 :param documentation: Metric HELP string.
89 :param typ: Metric type [counter|gauge|info].
91 :type documentation: str
95 u"counter", u"gauge", u"info"
97 self.metric_sample = namedtuple(
98 u"Sample", [u"name", u"labels", u"value", u"timestamp"]
101 if not re.compile(r"^[a-zA-Z_:][a-zA-Z0-9_:]*$").match(name):
102 raise ValueError(f"Invalid metric name: {name}!")
103 if typ not in self.metric_types:
104 raise ValueError(f"Invalid metric type: {typ}!")
107 self.documentation = documentation
111 def add_sample(self, name, labels, value, timestamp):
113 Add a sample (entry) to the metric.
115 :param name: Full metric name.
116 :param labels: Metric labels.
117 :param value: Metric value.
118 :param timestamp: Timestamp. Default to be when accessed.
121 :type value: int or float
122 :type timestamp: float
125 self.metric_sample(name, labels, value, timestamp)
128 def __eq__(self, other):
130 Check equality of added metric.
132 :param other: Metric to compare.
135 return (isinstance(other, Metric)
136 and self.name == other.name
137 and self.documentation == other.documentation
138 and self.type == other.type
139 and self.samples == other.samples)
143 Represantation as a string for a debug print.
146 f"Metric({self.name}, "
147 f"{self.documentation}, "
155 Abstract class for Metric implementation.
160 self, name, documentation, labelnames=(), namespace="",
161 subsystem="", labelvalues=None,
164 Metric initialization.
166 :param name: Metric name.
167 :param documentation: Metric HELP string.
168 :param labelnames: Metric label list.
169 :param namespace: Metric namespace (will be added as prefix).
170 :param subsystem: Metric susbsystem (will be added as prefix).
171 :param labelvalues: Metric label values.
173 :type documentation: str
174 :type labelnames: list
177 :type labelvalues: list
179 self._name = self.validate_name(name, namespace, subsystem)
180 self._labelnames = self.validate_labelnames(labelnames)
181 self._labelvalues = tuple(labelvalues or ())
182 self._documentation = documentation
184 if self._is_parent():
188 if self._is_observable():
192 def validate_name(name, namespace, subsystem):
194 Construct metric full name and validate naming convention.
196 :param name: Metric name.
197 :param namespace: Metric namespace (will be added as prefix).
198 :param subsystem: Metric susbsystem (will be added as prefix).
202 :returns: Metric full name.
204 :rasies ValueError: If name does not conform with naming conventions.
207 full_name += f"{namespace}_" if namespace else u""
208 full_name += f"{subsystem}_" if subsystem else u""
211 if not re.compile(r"^[a-zA-Z_:][a-zA-Z0-9_:]*$").match(full_name):
213 f"Invalid metric name: {full_name}!"
218 def validate_labelnames(labelnames):
220 Create label tuple and validate naming convention.
222 :param labelnames: Metric label list.
223 :type labelnames: list
224 :returns: Label names.
226 :rasies ValueError: If name does not conform with naming conventions.
228 labelnames = tuple(labelnames)
229 for label in labelnames:
230 if not re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$").match(label):
231 raise ValueError(f"Invalid label metric name: {label}!")
232 if re.compile(r"^__.*$").match(label):
233 raise ValueError(f"Reserved label metric name: {label}!")
236 def _is_observable(self):
238 Check whether this metric is observable, i.e.
239 * a metric without label names and values, or
240 * the child of a labelled metric.
245 return not self._labelnames or (self._labelnames and self._labelvalues)
247 def _is_parent(self):
249 Check whether metric is parent, i.e.
250 * a metric with label names but not its values.
255 return self._labelnames and not self._labelvalues
257 def _get_metric(self):
259 Returns metric that will handle samples.
261 :returns: Metric object.
264 return Metric(self._name, self._documentation, self._type)
268 Returns metric that will handle samples.
270 :returns: List of metric objects.
273 return [self._get_metric()]
277 Returns metric with samples.
279 :returns: List with metric object.
282 metric = self._get_metric()
283 for suffix, labels, value, timestamp in self.samples():
284 metric.add_sample(self._name + suffix, labels, value, timestamp)
287 def labels(self, *labelvalues, **labelkwargs):
289 Return the child for the given labelset.
291 :param labelvalues: Label values.
292 :param labelkwargs: Dictionary with label names and values.
293 :type labelvalues: list
294 :type labelkwargs: dict
295 :returns: Metric with labels and values.
297 :raises ValueError: If labels were not initialized.
298 :raises ValueError: If labels are already set (chaining).
299 :raises ValueError: If both parameters are passed.
300 :raises ValueError: If label values are not matching label names.
302 if not self._labelnames:
304 f"No label names were set when constructing {self}!"
307 if self._labelvalues:
309 f"{self} already has labels set; can not chain .labels() calls!"
312 if labelvalues and labelkwargs:
314 u"Can't pass both *args and **kwargs!"
318 if sorted(labelkwargs) != sorted(self._labelnames):
319 raise ValueError(u"Incorrect label names!")
320 labelvalues = tuple(labelkwargs[l] for l in self._labelnames)
322 if len(labelvalues) != len(self._labelnames):
323 raise ValueError(u"Incorrect label count!")
324 labelvalues = tuple(l for l in labelvalues)
326 if labelvalues not in self._metrics:
327 self._metrics[labelvalues] = self.__class__(
329 documentation=self._documentation,
330 labelnames=self._labelnames,
331 labelvalues=labelvalues
333 return self._metrics[labelvalues]
337 Returns samples wheter an object is parent or child.
339 :returns: List of Metric objects with values.
342 if self._is_parent():
343 return self._multi_samples()
344 return self._child_samples()
346 def _multi_samples(self):
348 Returns parent and its childs with its values.
350 :returns: List of Metric objects with values.
354 metrics = self._metrics.copy()
355 for labels, metric in metrics.items():
356 series_labels = list(zip(self._labelnames, labels))
357 for suffix, sample_labels, value, timestamp in metric.samples():
359 suffix, dict(series_labels + list(sample_labels.items())),
363 def _child_samples(self):
365 Returns child with its values. Should be implemented by child class.
367 :raises NotImplementedError: If implementation in not in subclass.
369 raise NotImplementedError(
370 f"_child_samples() must be implemented by {self}!"
373 def _metric_init(self):
375 Initialize the metric object as a child.
377 :raises NotImplementedError: If implementation in not in subclass.
379 raise NotImplementedError(
380 f"_metric_init() must be implemented by {self}!"
385 String for a debug print.
387 return f"{self._type}:{self._name}"
391 Represantation as a string for a debug print.
393 metric_type = type(self)
394 return f"{metric_type.__module__}.{metric_type.__name__}({self._name})"
397 class Counter(MetricBase):
399 A Counter tracks counts of events or running totals.
412 Initialize the Counter metric object.
414 :param name: Metric name.
415 :param documentation: Metric HELP string.
416 :param labelnames: Metric label list.
417 :param namespace: Metric namespace (will be added as prefix).
418 :param subsystem: Metric susbsystem (will be added as prefix).
419 :param labelvalues: Metric label values.
421 :type documentation: str
422 :type labelnames: list
425 :type labelvalues: list
427 super(Counter, self).__init__(
429 documentation=documentation,
430 labelnames=labelnames,
433 labelvalues=labelvalues,
436 def _metric_init(self):
438 Initialize counter value.
440 self._value = Value()
442 def inc(self, amount=1):
444 Increment counter by the given amount.
446 :param amount: Amount to increment.
447 :type amount: int or float
448 :raises ValueError: If amout is not positive.
452 u"Counters can only be incremented by non-negative amounts."
454 self._value.inc(amount)
456 def _child_samples(self):
458 Returns list of child samples.
460 :returns: List of child samples.
463 return ((u"", {}, self._value.get(), self._value.get_timestamp()),)
466 class Gauge(MetricBase):
468 Gauge metric, to report instantaneous values.
481 Initialize the Gauge metric object.
483 :param name: Metric name.
484 :param documentation: Metric HELP string.
485 :param labelnames: Metric label list.
486 :param namespace: Metric namespace (will be added as prefix).
487 :param subsystem: Metric susbsystem (will be added as prefix).
488 :param labelvalues: Metric label values.
490 :type documentation: str
491 :type labelnames: list
494 :type labelvalues: list
496 super(Gauge, self).__init__(
498 documentation=documentation,
499 labelnames=labelnames,
502 labelvalues=labelvalues,
505 def _metric_init(self):
507 Initialize gauge value.
509 self._value = Value()
511 def inc(self, amount=1):
513 Increment gauge by the given amount.
515 :param amount: Amount to increment.
516 :type amount: int or float
518 self._value.inc(amount)
520 def dec(self, amount=1):
522 Decrement gauge by the given amount.
524 :param amount: Amount to decrement.
525 :type amount: int or float
527 self._value.inc(-amount)
529 def set(self, value):
531 Set gauge to the given value.
533 :param amount: Value to set.
534 :type amount: int or float
536 self._value.set(float(value))
538 def _child_samples(self):
540 Returns list of child samples.
542 :returns: List of child samples.
545 return ((u"", {}, self._value.get(), self._value.get_timestamp()),)
548 class Info(MetricBase):
550 Info metric, key-value pairs.
563 Initialize the Info metric object.
565 :param name: Metric name.
566 :param documentation: Metric HELP string.
567 :param labelnames: Metric label list.
568 :param namespace: Metric namespace (will be added as prefix).
569 :param subsystem: Metric susbsystem (will be added as prefix).
570 :param labelvalues: Metric label values.
572 :type documentation: str
573 :type labelnames: list
576 :type labelvalues: list
578 super(Info, self).__init__(
580 documentation=documentation,
581 labelnames=labelnames,
584 labelvalues=labelvalues,
587 def _metric_init(self):
589 Initialize gauge value and time it was created.
591 self._labelname_set = set(self._labelnames)
595 def info(self, value):
597 Set info to the given value.
599 :param amount: Value to set.
600 :type amount: int or float
601 :raises ValueError: If lables are overlapping.
603 if self._labelname_set.intersection(value.keys()):
605 u"Overlapping labels for Info metric, "
606 f"metric: {self._labelnames} child: {value}!"
609 self._value = dict(value)
611 def _child_samples(self):
613 Returns list of child samples.
615 :returns: List of child samples.
619 return ((u"_info", self._value, 1.0, monotonic()),)