Index: infra_libs/ts_mon/metrics.py |
diff --git a/infra_libs/ts_mon/metrics.py b/infra_libs/ts_mon/metrics.py |
deleted file mode 100644 |
index fa986e6e115441c8eaaf97f57c1577658d5e0337..0000000000000000000000000000000000000000 |
--- a/infra_libs/ts_mon/metrics.py |
+++ /dev/null |
@@ -1,434 +0,0 @@ |
-# Copyright 2015 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-"""Classes representing individual metrics that can be sent.""" |
- |
- |
-import copy |
-import threading |
-import time |
- |
-from monacq.proto import metrics_pb2 |
- |
-from infra_libs.ts_mon import distribution |
-from infra_libs.ts_mon import errors |
-from infra_libs.ts_mon import interface |
- |
- |
-MICROSECONDS_PER_SECOND = 1000000 |
- |
- |
-class Metric(object): |
- """Abstract base class for a metric. |
- |
- A Metric is an attribute that may be monitored across many targets. Examples |
- include disk usage or the number of requests a server has received. A single |
- process may keep track of many metrics. |
- |
- Note that Metric objects may be initialized at any time (for example, at the |
- top of a library), but cannot be sent until the underlying Monitor object |
- has been set up (usually by the top-level process parsing the command line). |
- |
- A Metric can actually store multiple values that are identified by a set of |
- fields (which are themselves key-value pairs). Fields can be passed to the |
- set() or increment() methods to modify a particular value, or passed to the |
- constructor in which case they will be used as the defaults for this Metric. |
- |
- Do not directly instantiate an object of this class. |
- Use the concrete child classes instead: |
- * StringMetric for metrics with string value |
- * BooleanMetric for metrics with boolean values |
- * CounterMetric for metrics with monotonically increasing integer values |
- * GaugeMetric for metrics with arbitrarily varying integer values |
- * CumulativeMetric for metrics with monotonically increasing float values |
- * FloatMetric for metrics with arbitrarily varying float values |
- """ |
- |
- _initial_value = None |
- |
- def __init__(self, name, target=None, fields=None): |
- """Create an instance of a Metric. |
- |
- Args: |
- name (str): the file-like name of this metric |
- fields (dict): a set of key-value pairs to be set as default metric fields |
- target (Target): a Target to be used with this metric. This should be |
- specified only rarely; usually the library's default |
- Target will be used (set up by the top-level process). |
- """ |
- self._name = name.lstrip('/') |
- self._values = {} |
- self._target = target |
- fields = fields or {} |
- if len(fields) > 7: |
- raise errors.MonitoringTooManyFieldsError(self._name, fields) |
- self._fields = fields |
- self._normalized_fields = self._normalize_fields(self._fields) |
- self._thread_lock = threading.Lock() |
- |
- interface.register(self) |
- |
- def unregister(self): |
- interface.unregister(self) |
- |
- def serialize_to(self, collection_pb, default_target=None, loop_action=None): |
- """Generate metrics_pb2.MetricsData messages for this metric. |
- |
- Args: |
- collection_pb (metrics_pb2.MetricsCollection): protocol buffer into which |
- to add the current metric values. |
- default_target (Target): a Target to use if self._target is not set. |
- loop_action (function(metrics_pb2.MetricsCollection)): a function that we |
- must call with the collection_pb every loop iteration. |
- |
- Raises: |
- MonitoringNoConfiguredTargetError: if neither self._target nor |
- default_target is set |
- """ |
- |
- for fields, value in self._values.iteritems(): |
- if callable(loop_action): |
- loop_action(collection_pb) |
- metric_pb = collection_pb.data.add() |
- metric_pb.metric_name_prefix = '/chrome/infra/' |
- metric_pb.name = self._name |
- |
- self._populate_value(metric_pb, value) |
- self._populate_fields(metric_pb, fields) |
- |
- if self._target: |
- self._target._populate_target_pb(metric_pb) |
- elif default_target: |
- default_target._populate_target_pb(metric_pb) |
- else: |
- raise errors.MonitoringNoConfiguredTargetError(self._name) |
- |
- def _populate_fields(self, metric, fields): |
- """Fill in the fields attribute of a metric protocol buffer. |
- |
- Args: |
- metric (metrics_pb2.MetricsData): a metrics protobuf to populate |
- fields (list of (key, value) tuples): normalized metric fields |
- |
- Raises: |
- MonitoringInvalidFieldTypeError: if a field has a value of unknown type |
- """ |
- for key, value in fields: |
- field = metric.fields.add() |
- field.name = key |
- if isinstance(value, basestring): |
- field.type = metrics_pb2.MetricsField.STRING |
- field.string_value = value |
- elif isinstance(value, bool): |
- field.type = metrics_pb2.MetricsField.BOOL |
- field.bool_value = value |
- elif isinstance(value, int): |
- field.type = metrics_pb2.MetricsField.INT |
- field.int_value = value |
- else: |
- raise errors.MonitoringInvalidFieldTypeError(self._name, key, value) |
- |
- def _normalize_fields(self, fields): |
- """Merges the fields with the default fields and returns something hashable. |
- |
- Args: |
- fields (dict): A dict of fields passed by the user, or None. |
- |
- Returns: |
- A tuple of (key, value) tuples, ordered by key. This whole tuple is used |
- as the key in the self._values dict to identify the cell for a value. |
- |
- Raises: |
- MonitoringTooManyFieldsError: if there are more than seven metric fields |
- """ |
- if fields is None: |
- return self._normalized_fields |
- |
- all_fields = copy.copy(self._fields) |
- all_fields.update(fields) |
- |
- if len(all_fields) > 7: |
- raise errors.MonitoringTooManyFieldsError(self._name, all_fields) |
- |
- return tuple(sorted(all_fields.iteritems())) |
- |
- def _set_and_send_value(self, value, fields): |
- """Called by subclasses to set a new value for this metric. |
- |
- Args: |
- value (see concrete class): the value of the metric to be set |
- fields (dict): additional metric fields to complement those on self |
- """ |
- self._values[self._normalize_fields(fields)] = value |
- interface.send(self) |
- |
- def _populate_value(self, metric, value): |
- """Fill in the the data values of a metric protocol buffer. |
- |
- Args: |
- metric (metrics_pb2.MetricsData): a metrics protobuf to populate |
- value (see concrete class): the value of the metric to be set |
- """ |
- raise NotImplementedError() |
- |
- def set(self, value, fields=None): |
- """Set a new value for this metric. Results in sending a new value. |
- |
- The subclass should do appropriate type checking on value and then call |
- self._set_and_send_value. |
- |
- Args: |
- value (see concrete class): the value of the metric to be set |
- fields (dict): additional metric fields to complement those on self |
- """ |
- raise NotImplementedError() |
- |
- def get(self, fields=None): |
- """Returns the current value for this metric.""" |
- return self._values.get(self._normalize_fields(fields), self._initial_value) |
- |
- def reset(self): |
- """Resets the current values for this metric to 0. Useful for tests.""" |
- self._values = {} |
- |
- |
-class StringMetric(Metric): |
- """A metric whose value type is a string.""" |
- |
- def _populate_value(self, metric, value): |
- metric.string_value = value |
- |
- def set(self, value, fields=None): |
- if not isinstance(value, basestring): |
- raise errors.MonitoringInvalidValueTypeError(self._name, value) |
- self._set_and_send_value(value, fields) |
- |
- |
-class BooleanMetric(Metric): |
- """A metric whose value type is a boolean.""" |
- |
- def _populate_value(self, metric, value): |
- metric.boolean_value = value |
- |
- def set(self, value, fields=None): |
- if not isinstance(value, bool): |
- raise errors.MonitoringInvalidValueTypeError(self._name, value) |
- self._set_and_send_value(value, fields) |
- |
- def toggle(self, fields=None): |
- self.set(not self.get(fields), fields) |
- |
- |
-class NumericMetric(Metric): # pylint: disable=abstract-method |
- """Abstract base class for numeric (int or float) metrics.""" |
- #TODO(agable): Figure out if there's a way to send units with these metrics. |
- |
- def increment(self, fields=None): |
- self.increment_by(1, fields) |
- |
- def increment_by(self, step, fields=None): |
- if self.get(fields) is None: |
- raise errors.MonitoringIncrementUnsetValueError(self._name) |
- with self._thread_lock: |
- self.set(self.get(fields) + step, fields) |
- |
- |
-class CounterMetric(NumericMetric): |
- """A metric whose value type is a monotonically increasing integer.""" |
- |
- _initial_value = 0 |
- |
- def __init__( |
- self, name, target=None, fields=None, start_time=None, time_fn=time.time): |
- super(CounterMetric, self).__init__(name, target=target, fields=fields) |
- self._start_time = start_time or int(time_fn() * MICROSECONDS_PER_SECOND) |
- |
- def _populate_value(self, metric, value): |
- metric.counter = value |
- metric.start_timestamp_us = self._start_time |
- |
- def set(self, value, fields=None): |
- if not isinstance(value, (int, long)): |
- raise errors.MonitoringInvalidValueTypeError(self._name, value) |
- if value < self.get(fields): |
- raise errors.MonitoringDecreasingValueError( |
- self._name, self.get(fields), value) |
- self._set_and_send_value(value, fields) |
- |
- |
-class GaugeMetric(NumericMetric): |
- """A metric whose value type is an integer.""" |
- |
- def _populate_value(self, metric, value): |
- metric.gauge = value |
- |
- def set(self, value, fields=None): |
- if not isinstance(value, (int, long)): |
- raise errors.MonitoringInvalidValueTypeError(self._name, value) |
- self._set_and_send_value(value, fields) |
- |
- |
-class CumulativeMetric(NumericMetric): |
- """A metric whose value type is a monotonically increasing float.""" |
- |
- _initial_value = 0.0 |
- |
- def __init__( |
- self, name, target=None, fields=None, start_time=None, time_fn=time.time): |
- super(CumulativeMetric, self).__init__(name, target=target, fields=fields) |
- self._start_time = start_time or int(time_fn() * MICROSECONDS_PER_SECOND) |
- |
- def _populate_value(self, metric, value): |
- metric.cumulative_double_value = value |
- metric.start_timestamp_us = self._start_time |
- |
- def set(self, value, fields=None): |
- if not isinstance(value, (float, int)): |
- raise errors.MonitoringInvalidValueTypeError(self._name, value) |
- if value < self.get(fields): |
- raise errors.MonitoringDecreasingValueError( |
- self._name, self.get(fields), value) |
- self._set_and_send_value(float(value), fields) |
- |
- |
-class FloatMetric(NumericMetric): |
- """A metric whose value type is a float.""" |
- |
- def _populate_value(self, metric, value): |
- metric.noncumulative_double_value = value |
- |
- def set(self, value, fields=None): |
- if not isinstance(value, (float, int)): |
- raise errors.MonitoringInvalidValueTypeError(self._name, value) |
- self._set_and_send_value(float(value), fields) |
- |
- |
-class DistributionMetric(Metric): |
- """A metric that holds a distribution of values. |
- |
- By default buckets are chosen from a geometric progression, each bucket being |
- approximately 1.59 times bigger than the last. In practice this is suitable |
- for many kinds of data, but you may want to provide a FixedWidthBucketer or |
- GeometricBucketer with different parameters.""" |
- |
- CANONICAL_SPEC_TYPES = { |
- 2: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_2, |
- 10**0.2: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_10_P_0_2, |
- 10: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_10, |
- } |
- |
- def __init__(self, name, is_cumulative=True, bucketer=None, target=None, |
- fields=None, start_time=None, time_fn=time.time): |
- super(DistributionMetric, self).__init__(name, target, fields) |
- self._start_time = start_time or int(time_fn() * MICROSECONDS_PER_SECOND) |
- |
- if bucketer is None: |
- bucketer = distribution.GeometricBucketer() |
- |
- self.is_cumulative = is_cumulative |
- self.bucketer = bucketer |
- |
- def _populate_value(self, metric, value): |
- pb = metric.distribution |
- |
- pb.is_cumulative = self.is_cumulative |
- metric.start_timestamp_us = self._start_time |
- |
- # Copy the bucketer params. |
- if (value.bucketer.width == 0 and |
- value.bucketer.growth_factor in self.CANONICAL_SPEC_TYPES): |
- pb.spec_type = self.CANONICAL_SPEC_TYPES[value.bucketer.growth_factor] |
- else: |
- pb.spec_type = metrics_pb2.PrecomputedDistribution.CUSTOM_PARAMETERIZED |
- pb.width = value.bucketer.width |
- pb.growth_factor = value.bucketer.growth_factor |
- pb.num_buckets = value.bucketer.num_finite_buckets |
- |
- # Copy the distribution bucket values. Only include the finite buckets, not |
- # the overflow buckets on each end. |
- pb.bucket.extend(self._running_zero_generator( |
- value.buckets.get(i, 0) for i in |
- xrange(1, value.bucketer.total_buckets - 1))) |
- |
- # Add the overflow buckets if present. |
- if value.bucketer.underflow_bucket in value.buckets: |
- pb.underflow = value.buckets[value.bucketer.underflow_bucket] |
- if value.bucketer.overflow_bucket in value.buckets: |
- pb.overflow = value.buckets[value.bucketer.overflow_bucket] |
- |
- if value.count != 0: |
- pb.mean = float(value.sum) / value.count |
- |
- @staticmethod |
- def _running_zero_generator(iterable): |
- """Compresses sequences of zeroes in the iterable into negative zero counts. |
- |
- For example an input of [1, 0, 0, 0, 2] is converted to [1, -3, 2]. |
- """ |
- |
- count = 0 |
- |
- for value in iterable: |
- if value == 0: |
- count += 1 |
- else: |
- if count != 0: |
- yield -count |
- count = 0 |
- yield value |
- |
- def add(self, value, fields=None): |
- with self._thread_lock: |
- dist = self.get(fields) |
- if dist is None: |
- dist = distribution.Distribution(self.bucketer) |
- |
- dist.add(value) |
- self._set_and_send_value(dist, fields) |
- |
- def set(self, value, fields=None): |
- """Replaces the distribution with the given fields with another one. |
- |
- This only makes sense on non-cumulative DistributionMetrics. |
- |
- Args: |
- value: A infra_libs.ts_mon.Distribution. |
- """ |
- |
- if self.is_cumulative: |
- raise TypeError( |
- 'Cannot set() a cumulative DistributionMetric (use add() instead)') |
- |
- if not isinstance(value, distribution.Distribution): |
- raise errors.MonitoringInvalidValueTypeError(self._name, value) |
- |
- self._set_and_send_value(value, fields) |
- |
- |
-class CumulativeDistributionMetric(DistributionMetric): |
- """A DistributionMetric with is_cumulative set to True.""" |
- |
- def __init__( |
- self, name, bucketer=None, target=None, fields=None, time_fn=time.time): |
- super(CumulativeDistributionMetric, self).__init__( |
- name, |
- is_cumulative=True, |
- bucketer=bucketer, |
- target=target, |
- fields=fields, |
- time_fn=time_fn) |
- |
- |
-class NonCumulativeDistributionMetric(DistributionMetric): |
- """A DistributionMetric with is_cumulative set to False.""" |
- |
- def __init__( |
- self, name, bucketer=None, target=None, fields=None, time_fn=time.time): |
- super(NonCumulativeDistributionMetric, self).__init__( |
- name, |
- is_cumulative=False, |
- bucketer=bucketer, |
- target=target, |
- fields=fields, |
- time_fn=time_fn) |