Issue 623854: Support unit annotations in ts_mon metrics

For go-implementation, please find the following CLs:
- https://codereview.chromium.org/2123853002
- https://codereview.chromium.org/2125943003

BUG=623854

Review-Url: https://codereview.chromium.org/2111473003
Cr-Mirrored-From: https://chromium.googlesource.com/infra/infra
Cr-Mirrored-Commit: 9bf7fbe68e4dc69611e78ee0b43c44c258c49d3b
diff --git a/ts_mon/__init__.py b/ts_mon/__init__.py
index 7c08de6..bda03e9 100644
--- a/ts_mon/__init__.py
+++ b/ts_mon/__init__.py
@@ -33,6 +33,7 @@
 from infra_libs.ts_mon.common.metrics import FloatMetric
 from infra_libs.ts_mon.common.metrics import GaugeMetric
 from infra_libs.ts_mon.common.metrics import NonCumulativeDistributionMetric
+from infra_libs.ts_mon.common.metrics import MetricsDataUnits
 from infra_libs.ts_mon.common.metrics import StringMetric
 
 from infra_libs.ts_mon.common.targets import TaskTarget
diff --git a/ts_mon/common/metrics.py b/ts_mon/common/metrics.py
index 33a5437..a9ee417 100644
--- a/ts_mon/common/metrics.py
+++ b/ts_mon/common/metrics.py
@@ -32,6 +32,12 @@
   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.
 
+  The unit of measurement for Metric data can be specified with MetricsDataUnits
+  when a Metric object is created:
+  e.g., MetricsDataUnits.SECONDS, MetricsDataUnits.BYTES, and etc..,
+  A full list of supported units can be found in the following protobuf file
+  : infra_libs/ts_mon/protos/metrics.proto
+
   Do not directly instantiate an object of this class.
   Use the concrete child classes instead:
   * StringMetric for metrics with string value
@@ -44,7 +50,7 @@
   See http://go/inframon-doc for help designing and using your metrics.
   """
 
-  def __init__(self, name, fields=None, description=None):
+  def __init__(self, name, fields=None, description=None, units=None):
     """Create an instance of a Metric.
 
     Args:
@@ -52,6 +58,9 @@
       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.
+      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.
     """
     self._name = name.lstrip('/')
     self._start_time = None
@@ -61,6 +70,7 @@
     self._fields = fields
     self._normalized_fields = self._normalize_fields(self._fields)
     self._description = description
+    self._units = units
 
     interface.register(self)
 
@@ -98,6 +108,8 @@
     metric_pb.name = self._name
     if self._description is not None:
       metric_pb.description = self._description
+    if self._units is not None:
+      metric_pb.units = self._units
 
     self._populate_value(metric_pb, value, start_time)
     self._populate_fields(metric_pb, fields)
@@ -250,9 +262,10 @@
 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, fields=None, start_time=None, description=None,
+               units=None):
     super(CounterMetric, self).__init__(
-        name, fields=fields, description=description)
+        name, fields=fields, description=description, units=units)
     self._start_time = start_time
 
   def _populate_value(self, metric, value, start_time):
@@ -291,9 +304,10 @@
 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, fields=None, start_time=None, description=None,
+               units=None):
     super(CumulativeMetric, self).__init__(
-        name, fields=fields, description=description)
+        name, fields=fields, description=description, units=units)
     self._start_time = start_time
 
   def _populate_value(self, metric, value, start_time):
@@ -339,9 +353,9 @@
   }
 
   def __init__(self, name, is_cumulative=True, bucketer=None, fields=None,
-               start_time=None, description=None):
+               start_time=None, description=None, units=None):
     super(DistributionMetric, self).__init__(
-        name, fields=fields, description=description)
+        name, fields=fields, description=description, units=units)
     self._start_time = start_time
 
     if bucketer is None:
@@ -435,13 +449,14 @@
   """A DistributionMetric with is_cumulative set to True."""
 
   def __init__(self, name, bucketer=None, fields=None,
-               description=None):
+               description=None, units=None):
     super(CumulativeDistributionMetric, self).__init__(
         name,
         is_cumulative=True,
         bucketer=bucketer,
         fields=fields,
-        description=description)
+        description=description,
+        units=units)
 
   def is_cumulative(self):
     return True
@@ -451,13 +466,28 @@
   """A DistributionMetric with is_cumulative set to False."""
 
   def __init__(self, name, bucketer=None, fields=None,
-               description=None):
+               description=None, units=None):
     super(NonCumulativeDistributionMetric, self).__init__(
         name,
         is_cumulative=False,
         bucketer=bucketer,
         fields=fields,
-        description=description)
+        description=description,
+        units=units)
 
   def is_cumulative(self):
     return False
+
+
+class MetaMetricsDataUnits(type):
+  """Metaclass to populate the enum values of metrics_pb2.MetricsData.Units."""
+  def __new__(mcs, name, bases, attrs):
+    attrs.update(metrics_pb2.MetricsData.Units.items())
+    return super(MetaMetricsDataUnits, mcs).__new__(mcs, name, bases, attrs)
+
+
+class MetricsDataUnits(object):
+  """An enumeration class for units of measurement for Metrics data.
+  See infra_libs/ts_mon/protos/metrics.proto for a full list of supported units.
+  """
+  __metaclass__ = MetaMetricsDataUnits
diff --git a/ts_mon/common/test/metrics_test.expected/MetricTest.test_serialize_with_units.json b/ts_mon/common/test/metrics_test.expected/MetricTest.test_serialize_with_units.json
new file mode 100644
index 0000000..15c7037
--- /dev/null
+++ b/ts_mon/common/test/metrics_test.expected/MetricTest.test_serialize_with_units.json
@@ -0,0 +1,26 @@
+[
+  "data {",
+  "  name: \"test\"",
+  "  metric_name_prefix: \"/chrome/infra/\"",
+  "  network_device {",
+  "    alertable: true",
+  "    realm: \"ACQ_CHROME\"",
+  "    metro: \"reg\"",
+  "    role: \"role\"",
+  "    hostname: \"host\"",
+  "    hostgroup: \"net\"",
+  "  }",
+  "  fields {",
+  "    name: \"bar\"",
+  "    type: INT",
+  "    int_value: 1",
+  "  }",
+  "  fields {",
+  "    name: \"baz\"",
+  "    type: BOOL",
+  "    bool_value: false",
+  "  }",
+  "  gauge: 1",
+	"  units: SECONDS",
+  "}"
+]
diff --git a/ts_mon/common/test/metrics_test.py b/ts_mon/common/test/metrics_test.py
index a09795d..e89ebc4 100644
--- a/ts_mon/common/test/metrics_test.py
+++ b/ts_mon/common/test/metrics_test.py
@@ -62,6 +62,15 @@
     m.serialize_to(p, 1234, (('bar', 1), ('baz', False)), m.get(), t)
     return str(p).splitlines()
 
+  def test_serialize_with_units(self):
+    t = targets.DeviceTarget('reg', 'role', 'net', 'host')
+    m = metrics.GaugeMetric('test', units=metrics.MetricsDataUnits.SECONDS)
+    m.set(1)
+    p = metrics_pb2.MetricsCollection()
+    m.serialize_to(p, 1234, (('bar', 1), ('baz', False)), m.get(), t)
+    self.assertEquals(p.data[0].units, metrics.MetricsDataUnits.SECONDS)
+    return str(p).splitlines()
+
   def test_serialize_too_many_fields(self):
     m = metrics.StringMetric('test', fields={'a': 1, 'b': 2, 'c': 3, 'd': 4})
     m.set('val', fields={'e': 5, 'f': 6, 'g': 7})