Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(249)

Unified Diff: client/third_party/infra_libs/ts_mon/common/metrics.py

Issue 2705273003: Roll infra_libs and gae_ts_mon in luci-py, and add field_specs to all metrics (Closed)
Patch Set: Rebase Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: client/third_party/infra_libs/ts_mon/common/metrics.py
diff --git a/client/third_party/infra_libs/ts_mon/common/metrics.py b/client/third_party/infra_libs/ts_mon/common/metrics.py
index 22c138e7818132d3d0f1601043f47a92b2f490b3..9cb565abca6f178f1122c9f2c8da6fad8ebcedee 100644
--- a/client/third_party/infra_libs/ts_mon/common/metrics.py
+++ b/client/third_party/infra_libs/ts_mon/common/metrics.py
@@ -5,6 +5,7 @@
"""Classes representing individual metrics that can be sent."""
import copy
+import re
from infra_libs.ts_mon.protos.current import metrics_pb2
from infra_libs.ts_mon.protos.new import metrics_pb2 as new_metrics_pb2
@@ -17,6 +18,59 @@ from infra_libs.ts_mon.common import interface
MICROSECONDS_PER_SECOND = 1000000
+class Field(object):
+ FIELD_NAME_PATTERN = re.compile(r'[A-Za-z_][A-Za-z0-9_]*')
+
+ allowed_python_types = None
+ v1_type = None
+ v2_type = None
+ v1_field = None
+ v2_field = None
+
+ def __init__(self, name):
+ if not self.FIELD_NAME_PATTERN.match(name):
+ raise errors.MetricDefinitionError(
+ 'Invalid metric field name "%s" - must match the regex "%s"' % (
+ name, self.FIELD_NAME_PATTERN.pattern))
+
+ self.name = name
+
+ def validate_value(self, metric_name, value):
+ if not isinstance(value, self.allowed_python_types):
+ raise errors.MonitoringInvalidFieldTypeError(
+ metric_name, self.name, value)
+
+ def populate_proto_v1(self, proto, value):
+ setattr(proto, self.v1_field, value)
+
+ def populate_proto_v2(self, proto, value):
+ setattr(proto, self.v2_field, value)
+
+
+class StringField(Field):
+ allowed_python_types = basestring
+ v1_type = metrics_pb2.MetricsField.STRING
+ v2_type = new_metrics_pb2.MetricsDataSet.MetricFieldDescriptor.STRING
+ v1_field = 'string_value'
+ v2_field = 'string_value'
+
+
+class IntegerField(Field):
+ allowed_python_types = (int, long)
+ v1_type = metrics_pb2.MetricsField.INT
+ v2_type = new_metrics_pb2.MetricsDataSet.MetricFieldDescriptor.INT64
+ v1_field = 'int_value'
+ v2_field = 'int64_value'
+
+
+class BooleanField(Field):
+ allowed_python_types = bool
+ v1_type = metrics_pb2.MetricsField.BOOL
+ v2_type = new_metrics_pb2.MetricsDataSet.MetricFieldDescriptor.BOOL
+ v1_field = 'bool_value'
+ v2_field = 'bool_value'
+
+
class Metric(object):
"""Abstract base class for a metric.
@@ -51,25 +105,38 @@ class Metric(object):
See http://go/inframon-doc for help designing and using your metrics.
"""
- def __init__(self, name, fields=None, description=None, units=None):
+ def __init__(self, name, description, field_spec, units=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
description (string): help string for the metric. Should be enough to
know what the metric is about.
+ field_spec (list): a list of Field subclasses to define the fields that
+ are allowed on this metric. Pass a list of either
+ StringField, IntegerField or BooleanField here.
units (int): the unit used to measure data for given
metric. Please use the attributes of MetricDataUnit to find
valid integer values for this argument.
"""
+ field_spec = field_spec or []
+
self._name = name.lstrip('/')
+
+ if not isinstance(description, basestring):
+ raise errors.MetricDefinitionError('Metric description must be a string')
+ if not description:
+ raise errors.MetricDefinitionError('Metric must have a description')
+ if (not isinstance(field_spec, (list, tuple)) or
+ any(not isinstance(x, Field) for x in field_spec)):
+ raise errors.MetricDefinitionError(
+ 'Metric constructor takes a list of Fields, or None')
+ if len(field_spec) > 7:
+ raise errors.MonitoringTooManyFieldsError(self._name, field_spec)
+
self._start_time = None
- 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._field_spec = field_spec
+ self._sorted_field_names = sorted(x.name for x in field_spec)
self._description = description
self._units = units
@@ -86,11 +153,6 @@ class Metric(object):
def is_cumulative(self):
raise NotImplementedError()
- def __eq__(self, other):
- return (self.name == other.name and
- self._fields == other._fields and
- type(self) == type(other))
-
def unregister(self):
interface.unregister(self)
@@ -104,7 +166,7 @@ class Metric(object):
else:
return '{unknown}'
- def _populate_data_set(self, data_set, fields):
+ def _populate_data_set(self, data_set):
"""Populate MetricsDataSet."""
data_set.metric_name = '%s%s' % (interface.state.metric_name_prefix,
self._name)
@@ -117,7 +179,7 @@ class Metric(object):
data_set.stream_kind = new_metrics_pb2.GAUGE
self._populate_value_type(data_set)
- self._populate_field_descriptors(data_set, fields)
+ self._populate_field_descriptors(data_set)
def _populate_data(self, data, start_time, end_time, fields, value):
"""Populate a new metrics_pb2.MetricsData.
@@ -145,8 +207,7 @@ class Metric(object):
metric_pb.metric_name_prefix = interface.state.metric_name_prefix
metric_pb.name = self._name
- if self._description is not None:
- metric_pb.description = self._description
+ metric_pb.description = self._description
if self._units is not None:
metric_pb.units = self._units
@@ -155,100 +216,71 @@ class Metric(object):
target._populate_target_pb(metric_pb)
- def _populate_field_descriptors(self, data_set, fields):
+ def _populate_field_descriptors(self, data_set):
"""Populate `field_descriptor` in MetricsDataSet.
Args:
- data_set (new_metrics_pb2.MetricsDataSet): a data set protobuf to
- populate
- fields (list of (key, value) tuples): normalized metric fields
-
- Raises:
- MonitoringInvalidFieldTypeError: if a field has a value of unknown type
+ data_set (new_metrics_pb2.MetricsDataSet): a data set protobuf to populate
"""
- field_type = new_metrics_pb2.MetricsDataSet.MetricFieldDescriptor
- for key, value in fields:
+ for spec in self._field_spec:
descriptor = data_set.field_descriptor.add()
- descriptor.name = key
- if isinstance(value, basestring):
- descriptor.field_type = field_type.STRING
- elif isinstance(value, bool):
- descriptor.field_type = field_type.BOOL
- elif isinstance(value, int):
- descriptor.field_type = field_type.INT64
- else:
- raise errors.MonitoringInvalidFieldTypeError(self._name, key, value)
+ descriptor.name = spec.name
+ descriptor.field_type = spec.v2_type
- def _populate_fields_new(self, data, fields):
+ def _populate_fields_new(self, data, field_values):
"""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
+ field_values (tuple): field values
"""
- for key, value in fields:
+ for spec, value in zip(self._field_spec, field_values):
field = data.field.add()
- field.name = key
- if isinstance(value, basestring):
- field.string_value = value
- elif isinstance(value, bool):
- field.bool_value = value
- elif isinstance(value, int):
- field.int64_value = value
- else:
- raise errors.MonitoringInvalidFieldTypeError(self._name, key, value)
+ field.name = spec.name
+ spec.populate_proto_v2(field, value)
- def _populate_fields(self, metric, fields):
+ def _populate_fields(self, metric, field_values):
"""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
+ field_values (tuple): field values
"""
- for key, value in fields:
+ for spec, value in zip(self._field_spec, field_values):
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)
+ field.name = spec.name
+ field.type = spec.v1_type
+ spec.populate_proto_v1(field, value)
- def _normalize_fields(self, fields):
- """Merges the fields with the default fields and returns something hashable.
+ def _validate_fields(self, fields):
+ """Checks the correct number and types of field values were provided.
Args:
- fields (dict): A dict of fields passed by the user, or None.
+ fields (dict): A dict of field values given 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.
+ fields' values as a tuple, in the same order as the field_spec.
Raises:
- MonitoringTooManyFieldsError: if there are more than seven metric fields
+ WrongFieldsError: if you provide a different number of fields to those
+ the metric was defined with.
+ MonitoringInvalidFieldTypeError: if the field value was the wrong type for
+ the field spec.
"""
- if fields is None:
- return self._normalized_fields
+ fields = fields or {}
- all_fields = copy.copy(self._fields)
- all_fields.update(fields)
+ if not isinstance(fields, dict):
+ raise ValueError('fields should be a dict, got %r (%s)' % (
+ fields, type(fields)))
- if len(all_fields) > 7:
- raise errors.MonitoringTooManyFieldsError(self._name, all_fields)
+ if sorted(fields) != self._sorted_field_names:
+ raise errors.WrongFieldsError(
+ self.name, fields.keys(), self._sorted_field_names)
- return tuple(sorted(all_fields.iteritems()))
+ for spec in self._field_spec:
+ spec.validate_value(self.name, fields[spec.name])
+
+ return tuple(fields[spec.name] for spec in self._field_spec)
def _populate_value(self, metric, value, start_time):
"""Fill in the the data values of a metric protocol buffer.
@@ -286,7 +318,7 @@ class Metric(object):
Args:
value (see concrete class): the value of the metric to be set
- fields (dict): additional metric fields to complement those on self
+ fields (dict): metric field values
target_fields (dict): overwrite some of the default target fields
"""
raise NotImplementedError()
@@ -298,7 +330,7 @@ class Metric(object):
Instead use _incr with a modify_fn.
"""
return interface.state.store.get(
- self.name, self._normalize_fields(fields), target_fields)
+ self.name, self._validate_fields(fields), target_fields)
def get_all(self):
return interface.state.store.iter_field_values(self.name)
@@ -313,12 +345,14 @@ class Metric(object):
interface.state.store.reset_for_unittest(self.name)
def _set(self, fields, target_fields, value, enforce_ge=False):
- interface.state.store.set(self.name, self._normalize_fields(fields),
- target_fields, value, enforce_ge=enforce_ge)
+ interface.state.store.set(
+ self.name, self._validate_fields(fields), target_fields,
+ value, enforce_ge=enforce_ge)
def _incr(self, fields, target_fields, delta, modify_fn=None):
- interface.state.store.incr(self.name, self._normalize_fields(fields),
- target_fields, delta, modify_fn=modify_fn)
+ interface.state.store.incr(
+ self.name, self._validate_fields(fields), target_fields,
+ delta, modify_fn=modify_fn)
class StringMetric(Metric):
@@ -365,7 +399,6 @@ class BooleanMetric(Metric):
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, target_fields=None):
self._incr(fields, target_fields, 1)
@@ -377,10 +410,10 @@ class NumericMetric(Metric): # pylint: disable=abstract-method
class CounterMetric(NumericMetric):
"""A metric whose value type is a monotonically increasing integer."""
- def __init__(self, name, fields=None, start_time=None, description=None,
+ def __init__(self, name, description, field_spec=None, start_time=None,
units=None):
super(CounterMetric, self).__init__(
- name, fields=fields, description=description, units=units)
+ name, description, field_spec, units=units)
self._start_time = start_time
def _populate_value(self, metric, value, start_time):
@@ -431,10 +464,10 @@ class GaugeMetric(NumericMetric):
class CumulativeMetric(NumericMetric):
"""A metric whose value type is a monotonically increasing float."""
- def __init__(self, name, fields=None, start_time=None, description=None,
+ def __init__(self, name, description, field_spec=None, start_time=None,
units=None):
super(CumulativeMetric, self).__init__(
- name, fields=fields, description=description, units=units)
+ name, description, field_spec, units=units)
self._start_time = start_time
def _populate_value(self, metric, value, start_time):
@@ -477,7 +510,7 @@ class FloatMetric(NumericMetric):
return False
-class DistributionMetric(Metric):
+class _DistributionMetricBase(Metric):
"""A metric that holds a distribution of values.
By default buckets are chosen from a geometric progression, each bucket being
@@ -491,10 +524,10 @@ class DistributionMetric(Metric):
10: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_10,
}
- def __init__(self, name, is_cumulative=True, bucketer=None, fields=None,
- start_time=None, description=None, units=None):
- super(DistributionMetric, self).__init__(
- name, fields=fields, description=description, units=units)
+ def __init__(self, name, description, field_spec=None, is_cumulative=True,
+ bucketer=None, start_time=None, units=None):
+ super(_DistributionMetricBase, self).__init__(
+ name, description, field_spec, units=units)
self._start_time = start_time
if bucketer is None:
@@ -607,42 +640,32 @@ class DistributionMetric(Metric):
self._set(fields, target_fields, value)
def is_cumulative(self):
- raise NotImplementedError() # Keep this class abstract.
+ return self._is_cumulative
-class CumulativeDistributionMetric(DistributionMetric):
+class CumulativeDistributionMetric(_DistributionMetricBase):
"""A DistributionMetric with is_cumulative set to True."""
- def __init__(self, name, bucketer=None, fields=None,
- description=None, units=None):
+ def __init__(self, name, description, field_spec=None, bucketer=None,
+ units=None):
super(CumulativeDistributionMetric, self).__init__(
- name,
+ name, description, field_spec,
is_cumulative=True,
bucketer=bucketer,
- fields=fields,
- description=description,
units=units)
- def is_cumulative(self):
- return True
-
-class NonCumulativeDistributionMetric(DistributionMetric):
+class NonCumulativeDistributionMetric(_DistributionMetricBase):
"""A DistributionMetric with is_cumulative set to False."""
- def __init__(self, name, bucketer=None, fields=None,
- description=None, units=None):
+ def __init__(self, name, description, field_spec=None, bucketer=None,
+ units=None):
super(NonCumulativeDistributionMetric, self).__init__(
- name,
+ name, description, field_spec,
is_cumulative=False,
bucketer=bucketer,
- fields=fields,
- description=description,
units=units)
- def is_cumulative(self):
- return False
-
class MetaMetricsDataUnits(type):
"""Metaclass to populate the enum values of metrics_pb2.MetricsData.Units."""
« no previous file with comments | « client/third_party/infra_libs/ts_mon/common/interface.py ('k') | client/third_party/infra_libs/ts_mon/common/monitors.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698