| OLD | NEW | 
|---|
|  | (Empty) | 
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. |  | 
| 2 # Use of this source code is governed by a BSD-style license that can be |  | 
| 3 # found in the LICENSE file. |  | 
| 4 |  | 
| 5 """Classes representing individual metrics that can be sent.""" |  | 
| 6 |  | 
| 7 |  | 
| 8 import copy |  | 
| 9 import threading |  | 
| 10 import time |  | 
| 11 |  | 
| 12 from monacq.proto import metrics_pb2 |  | 
| 13 |  | 
| 14 from infra_libs.ts_mon import distribution |  | 
| 15 from infra_libs.ts_mon import errors |  | 
| 16 from infra_libs.ts_mon import interface |  | 
| 17 |  | 
| 18 |  | 
| 19 MICROSECONDS_PER_SECOND = 1000000 |  | 
| 20 |  | 
| 21 |  | 
| 22 class Metric(object): |  | 
| 23   """Abstract base class for a metric. |  | 
| 24 |  | 
| 25   A Metric is an attribute that may be monitored across many targets. Examples |  | 
| 26   include disk usage or the number of requests a server has received. A single |  | 
| 27   process may keep track of many metrics. |  | 
| 28 |  | 
| 29   Note that Metric objects may be initialized at any time (for example, at the |  | 
| 30   top of a library), but cannot be sent until the underlying Monitor object |  | 
| 31   has been set up (usually by the top-level process parsing the command line). |  | 
| 32 |  | 
| 33   A Metric can actually store multiple values that are identified by a set of |  | 
| 34   fields (which are themselves key-value pairs).  Fields can be passed to the |  | 
| 35   set() or increment() methods to modify a particular value, or passed to the |  | 
| 36   constructor in which case they will be used as the defaults for this Metric. |  | 
| 37 |  | 
| 38   Do not directly instantiate an object of this class. |  | 
| 39   Use the concrete child classes instead: |  | 
| 40   * StringMetric for metrics with string value |  | 
| 41   * BooleanMetric for metrics with boolean values |  | 
| 42   * CounterMetric for metrics with monotonically increasing integer values |  | 
| 43   * GaugeMetric for metrics with arbitrarily varying integer values |  | 
| 44   * CumulativeMetric for metrics with monotonically increasing float values |  | 
| 45   * FloatMetric for metrics with arbitrarily varying float values |  | 
| 46   """ |  | 
| 47 |  | 
| 48   _initial_value = None |  | 
| 49 |  | 
| 50   def __init__(self, name, target=None, fields=None): |  | 
| 51     """Create an instance of a Metric. |  | 
| 52 |  | 
| 53     Args: |  | 
| 54       name (str): the file-like name of this metric |  | 
| 55       fields (dict): a set of key-value pairs to be set as default metric fields |  | 
| 56       target (Target): a Target to be used with this metric. This should be |  | 
| 57                        specified only rarely; usually the library's default |  | 
| 58                        Target will be used (set up by the top-level process). |  | 
| 59     """ |  | 
| 60     self._name = name.lstrip('/') |  | 
| 61     self._values = {} |  | 
| 62     self._target = target |  | 
| 63     fields = fields or {} |  | 
| 64     if len(fields) > 7: |  | 
| 65       raise errors.MonitoringTooManyFieldsError(self._name, fields) |  | 
| 66     self._fields = fields |  | 
| 67     self._normalized_fields = self._normalize_fields(self._fields) |  | 
| 68     self._thread_lock = threading.Lock() |  | 
| 69 |  | 
| 70     interface.register(self) |  | 
| 71 |  | 
| 72   def unregister(self): |  | 
| 73     interface.unregister(self) |  | 
| 74 |  | 
| 75   def serialize_to(self, collection_pb, default_target=None, loop_action=None): |  | 
| 76     """Generate metrics_pb2.MetricsData messages for this metric. |  | 
| 77 |  | 
| 78     Args: |  | 
| 79       collection_pb (metrics_pb2.MetricsCollection): protocol buffer into which |  | 
| 80         to add the current metric values. |  | 
| 81       default_target (Target): a Target to use if self._target is not set. |  | 
| 82       loop_action (function(metrics_pb2.MetricsCollection)): a function that we |  | 
| 83         must call with the collection_pb every loop iteration. |  | 
| 84 |  | 
| 85     Raises: |  | 
| 86       MonitoringNoConfiguredTargetError: if neither self._target nor |  | 
| 87                                          default_target is set |  | 
| 88     """ |  | 
| 89 |  | 
| 90     for fields, value in self._values.iteritems(): |  | 
| 91       if callable(loop_action): |  | 
| 92         loop_action(collection_pb) |  | 
| 93       metric_pb = collection_pb.data.add() |  | 
| 94       metric_pb.metric_name_prefix = '/chrome/infra/' |  | 
| 95       metric_pb.name = self._name |  | 
| 96 |  | 
| 97       self._populate_value(metric_pb, value) |  | 
| 98       self._populate_fields(metric_pb, fields) |  | 
| 99 |  | 
| 100       if self._target: |  | 
| 101         self._target._populate_target_pb(metric_pb) |  | 
| 102       elif default_target: |  | 
| 103         default_target._populate_target_pb(metric_pb) |  | 
| 104       else: |  | 
| 105         raise errors.MonitoringNoConfiguredTargetError(self._name) |  | 
| 106 |  | 
| 107   def _populate_fields(self, metric, fields): |  | 
| 108     """Fill in the fields attribute of a metric protocol buffer. |  | 
| 109 |  | 
| 110     Args: |  | 
| 111       metric (metrics_pb2.MetricsData): a metrics protobuf to populate |  | 
| 112       fields (list of (key, value) tuples): normalized metric fields |  | 
| 113 |  | 
| 114     Raises: |  | 
| 115       MonitoringInvalidFieldTypeError: if a field has a value of unknown type |  | 
| 116     """ |  | 
| 117     for key, value in fields: |  | 
| 118       field = metric.fields.add() |  | 
| 119       field.name = key |  | 
| 120       if isinstance(value, basestring): |  | 
| 121         field.type = metrics_pb2.MetricsField.STRING |  | 
| 122         field.string_value = value |  | 
| 123       elif isinstance(value, bool): |  | 
| 124         field.type = metrics_pb2.MetricsField.BOOL |  | 
| 125         field.bool_value = value |  | 
| 126       elif isinstance(value, int): |  | 
| 127         field.type = metrics_pb2.MetricsField.INT |  | 
| 128         field.int_value = value |  | 
| 129       else: |  | 
| 130         raise errors.MonitoringInvalidFieldTypeError(self._name, key, value) |  | 
| 131 |  | 
| 132   def _normalize_fields(self, fields): |  | 
| 133     """Merges the fields with the default fields and returns something hashable. |  | 
| 134 |  | 
| 135     Args: |  | 
| 136       fields (dict): A dict of fields passed by the user, or None. |  | 
| 137 |  | 
| 138     Returns: |  | 
| 139       A tuple of (key, value) tuples, ordered by key.  This whole tuple is used |  | 
| 140       as the key in the self._values dict to identify the cell for a value. |  | 
| 141 |  | 
| 142     Raises: |  | 
| 143       MonitoringTooManyFieldsError: if there are more than seven metric fields |  | 
| 144     """ |  | 
| 145     if fields is None: |  | 
| 146       return self._normalized_fields |  | 
| 147 |  | 
| 148     all_fields = copy.copy(self._fields) |  | 
| 149     all_fields.update(fields) |  | 
| 150 |  | 
| 151     if len(all_fields) > 7: |  | 
| 152       raise errors.MonitoringTooManyFieldsError(self._name, all_fields) |  | 
| 153 |  | 
| 154     return tuple(sorted(all_fields.iteritems())) |  | 
| 155 |  | 
| 156   def _set_and_send_value(self, value, fields): |  | 
| 157     """Called by subclasses to set a new value for this metric. |  | 
| 158 |  | 
| 159     Args: |  | 
| 160       value (see concrete class): the value of the metric to be set |  | 
| 161       fields (dict): additional metric fields to complement those on self |  | 
| 162     """ |  | 
| 163     self._values[self._normalize_fields(fields)] = value |  | 
| 164     interface.send(self) |  | 
| 165 |  | 
| 166   def _populate_value(self, metric, value): |  | 
| 167     """Fill in the the data values of a metric protocol buffer. |  | 
| 168 |  | 
| 169     Args: |  | 
| 170       metric (metrics_pb2.MetricsData): a metrics protobuf to populate |  | 
| 171       value (see concrete class): the value of the metric to be set |  | 
| 172     """ |  | 
| 173     raise NotImplementedError() |  | 
| 174 |  | 
| 175   def set(self, value, fields=None): |  | 
| 176     """Set a new value for this metric. Results in sending a new value. |  | 
| 177 |  | 
| 178     The subclass should do appropriate type checking on value and then call |  | 
| 179     self._set_and_send_value. |  | 
| 180 |  | 
| 181     Args: |  | 
| 182       value (see concrete class): the value of the metric to be set |  | 
| 183       fields (dict): additional metric fields to complement those on self |  | 
| 184     """ |  | 
| 185     raise NotImplementedError() |  | 
| 186 |  | 
| 187   def get(self, fields=None): |  | 
| 188     """Returns the current value for this metric.""" |  | 
| 189     return self._values.get(self._normalize_fields(fields), self._initial_value) |  | 
| 190 |  | 
| 191   def reset(self): |  | 
| 192     """Resets the current values for this metric to 0.  Useful for tests.""" |  | 
| 193     self._values = {} |  | 
| 194 |  | 
| 195 |  | 
| 196 class StringMetric(Metric): |  | 
| 197   """A metric whose value type is a string.""" |  | 
| 198 |  | 
| 199   def _populate_value(self, metric, value): |  | 
| 200     metric.string_value = value |  | 
| 201 |  | 
| 202   def set(self, value, fields=None): |  | 
| 203     if not isinstance(value, basestring): |  | 
| 204       raise errors.MonitoringInvalidValueTypeError(self._name, value) |  | 
| 205     self._set_and_send_value(value, fields) |  | 
| 206 |  | 
| 207 |  | 
| 208 class BooleanMetric(Metric): |  | 
| 209   """A metric whose value type is a boolean.""" |  | 
| 210 |  | 
| 211   def _populate_value(self, metric, value): |  | 
| 212     metric.boolean_value = value |  | 
| 213 |  | 
| 214   def set(self, value, fields=None): |  | 
| 215     if not isinstance(value, bool): |  | 
| 216       raise errors.MonitoringInvalidValueTypeError(self._name, value) |  | 
| 217     self._set_and_send_value(value, fields) |  | 
| 218 |  | 
| 219   def toggle(self, fields=None): |  | 
| 220     self.set(not self.get(fields), fields) |  | 
| 221 |  | 
| 222 |  | 
| 223 class NumericMetric(Metric):  # pylint: disable=abstract-method |  | 
| 224   """Abstract base class for numeric (int or float) metrics.""" |  | 
| 225   #TODO(agable): Figure out if there's a way to send units with these metrics. |  | 
| 226 |  | 
| 227   def increment(self, fields=None): |  | 
| 228     self.increment_by(1, fields) |  | 
| 229 |  | 
| 230   def increment_by(self, step, fields=None): |  | 
| 231     if self.get(fields) is None: |  | 
| 232       raise errors.MonitoringIncrementUnsetValueError(self._name) |  | 
| 233     with self._thread_lock: |  | 
| 234       self.set(self.get(fields) + step, fields) |  | 
| 235 |  | 
| 236 |  | 
| 237 class CounterMetric(NumericMetric): |  | 
| 238   """A metric whose value type is a monotonically increasing integer.""" |  | 
| 239 |  | 
| 240   _initial_value = 0 |  | 
| 241 |  | 
| 242   def __init__( |  | 
| 243       self, name, target=None, fields=None, start_time=None, time_fn=time.time): |  | 
| 244     super(CounterMetric, self).__init__(name, target=target, fields=fields) |  | 
| 245     self._start_time = start_time or int(time_fn() * MICROSECONDS_PER_SECOND) |  | 
| 246 |  | 
| 247   def _populate_value(self, metric, value): |  | 
| 248     metric.counter = value |  | 
| 249     metric.start_timestamp_us = self._start_time |  | 
| 250 |  | 
| 251   def set(self, value, fields=None): |  | 
| 252     if not isinstance(value, (int, long)): |  | 
| 253       raise errors.MonitoringInvalidValueTypeError(self._name, value) |  | 
| 254     if value < self.get(fields): |  | 
| 255       raise errors.MonitoringDecreasingValueError( |  | 
| 256           self._name, self.get(fields), value) |  | 
| 257     self._set_and_send_value(value, fields) |  | 
| 258 |  | 
| 259 |  | 
| 260 class GaugeMetric(NumericMetric): |  | 
| 261   """A metric whose value type is an integer.""" |  | 
| 262 |  | 
| 263   def _populate_value(self, metric, value): |  | 
| 264     metric.gauge = value |  | 
| 265 |  | 
| 266   def set(self, value, fields=None): |  | 
| 267     if not isinstance(value, (int, long)): |  | 
| 268       raise errors.MonitoringInvalidValueTypeError(self._name, value) |  | 
| 269     self._set_and_send_value(value, fields) |  | 
| 270 |  | 
| 271 |  | 
| 272 class CumulativeMetric(NumericMetric): |  | 
| 273   """A metric whose value type is a monotonically increasing float.""" |  | 
| 274 |  | 
| 275   _initial_value = 0.0 |  | 
| 276 |  | 
| 277   def __init__( |  | 
| 278       self, name, target=None, fields=None, start_time=None, time_fn=time.time): |  | 
| 279     super(CumulativeMetric, self).__init__(name, target=target, fields=fields) |  | 
| 280     self._start_time = start_time or int(time_fn() * MICROSECONDS_PER_SECOND) |  | 
| 281 |  | 
| 282   def _populate_value(self, metric, value): |  | 
| 283     metric.cumulative_double_value = value |  | 
| 284     metric.start_timestamp_us = self._start_time |  | 
| 285 |  | 
| 286   def set(self, value, fields=None): |  | 
| 287     if not isinstance(value, (float, int)): |  | 
| 288       raise errors.MonitoringInvalidValueTypeError(self._name, value) |  | 
| 289     if value < self.get(fields): |  | 
| 290       raise errors.MonitoringDecreasingValueError( |  | 
| 291           self._name, self.get(fields), value) |  | 
| 292     self._set_and_send_value(float(value), fields) |  | 
| 293 |  | 
| 294 |  | 
| 295 class FloatMetric(NumericMetric): |  | 
| 296   """A metric whose value type is a float.""" |  | 
| 297 |  | 
| 298   def _populate_value(self, metric, value): |  | 
| 299     metric.noncumulative_double_value = value |  | 
| 300 |  | 
| 301   def set(self, value, fields=None): |  | 
| 302     if not isinstance(value, (float, int)): |  | 
| 303       raise errors.MonitoringInvalidValueTypeError(self._name, value) |  | 
| 304     self._set_and_send_value(float(value), fields) |  | 
| 305 |  | 
| 306 |  | 
| 307 class DistributionMetric(Metric): |  | 
| 308   """A metric that holds a distribution of values. |  | 
| 309 |  | 
| 310   By default buckets are chosen from a geometric progression, each bucket being |  | 
| 311   approximately 1.59 times bigger than the last.  In practice this is suitable |  | 
| 312   for many kinds of data, but you may want to provide a FixedWidthBucketer or |  | 
| 313   GeometricBucketer with different parameters.""" |  | 
| 314 |  | 
| 315   CANONICAL_SPEC_TYPES = { |  | 
| 316       2: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_2, |  | 
| 317       10**0.2: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_10_P_0_2, |  | 
| 318       10: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_10, |  | 
| 319   } |  | 
| 320 |  | 
| 321   def __init__(self, name, is_cumulative=True, bucketer=None, target=None, |  | 
| 322                fields=None, start_time=None, time_fn=time.time): |  | 
| 323     super(DistributionMetric, self).__init__(name, target, fields) |  | 
| 324     self._start_time = start_time or int(time_fn() * MICROSECONDS_PER_SECOND) |  | 
| 325 |  | 
| 326     if bucketer is None: |  | 
| 327       bucketer = distribution.GeometricBucketer() |  | 
| 328 |  | 
| 329     self.is_cumulative = is_cumulative |  | 
| 330     self.bucketer = bucketer |  | 
| 331 |  | 
| 332   def _populate_value(self, metric, value): |  | 
| 333     pb = metric.distribution |  | 
| 334 |  | 
| 335     pb.is_cumulative = self.is_cumulative |  | 
| 336     metric.start_timestamp_us = self._start_time |  | 
| 337 |  | 
| 338     # Copy the bucketer params. |  | 
| 339     if (value.bucketer.width == 0 and |  | 
| 340         value.bucketer.growth_factor in self.CANONICAL_SPEC_TYPES): |  | 
| 341       pb.spec_type = self.CANONICAL_SPEC_TYPES[value.bucketer.growth_factor] |  | 
| 342     else: |  | 
| 343       pb.spec_type = metrics_pb2.PrecomputedDistribution.CUSTOM_PARAMETERIZED |  | 
| 344       pb.width = value.bucketer.width |  | 
| 345       pb.growth_factor = value.bucketer.growth_factor |  | 
| 346       pb.num_buckets = value.bucketer.num_finite_buckets |  | 
| 347 |  | 
| 348     # Copy the distribution bucket values.  Only include the finite buckets, not |  | 
| 349     # the overflow buckets on each end. |  | 
| 350     pb.bucket.extend(self._running_zero_generator( |  | 
| 351         value.buckets.get(i, 0) for i in |  | 
| 352         xrange(1, value.bucketer.total_buckets - 1))) |  | 
| 353 |  | 
| 354     # Add the overflow buckets if present. |  | 
| 355     if value.bucketer.underflow_bucket in value.buckets: |  | 
| 356       pb.underflow = value.buckets[value.bucketer.underflow_bucket] |  | 
| 357     if value.bucketer.overflow_bucket in value.buckets: |  | 
| 358       pb.overflow = value.buckets[value.bucketer.overflow_bucket] |  | 
| 359 |  | 
| 360     if value.count != 0: |  | 
| 361       pb.mean = float(value.sum) / value.count |  | 
| 362 |  | 
| 363   @staticmethod |  | 
| 364   def _running_zero_generator(iterable): |  | 
| 365     """Compresses sequences of zeroes in the iterable into negative zero counts. |  | 
| 366 |  | 
| 367     For example an input of [1, 0, 0, 0, 2] is converted to [1, -3, 2]. |  | 
| 368     """ |  | 
| 369 |  | 
| 370     count = 0 |  | 
| 371 |  | 
| 372     for value in iterable: |  | 
| 373       if value == 0: |  | 
| 374         count += 1 |  | 
| 375       else: |  | 
| 376         if count != 0: |  | 
| 377           yield -count |  | 
| 378           count = 0 |  | 
| 379         yield value |  | 
| 380 |  | 
| 381   def add(self, value, fields=None): |  | 
| 382     with self._thread_lock: |  | 
| 383       dist = self.get(fields) |  | 
| 384       if dist is None: |  | 
| 385         dist = distribution.Distribution(self.bucketer) |  | 
| 386 |  | 
| 387       dist.add(value) |  | 
| 388       self._set_and_send_value(dist, fields) |  | 
| 389 |  | 
| 390   def set(self, value, fields=None): |  | 
| 391     """Replaces the distribution with the given fields with another one. |  | 
| 392 |  | 
| 393     This only makes sense on non-cumulative DistributionMetrics. |  | 
| 394 |  | 
| 395     Args: |  | 
| 396       value: A infra_libs.ts_mon.Distribution. |  | 
| 397     """ |  | 
| 398 |  | 
| 399     if self.is_cumulative: |  | 
| 400       raise TypeError( |  | 
| 401           'Cannot set() a cumulative DistributionMetric (use add() instead)') |  | 
| 402 |  | 
| 403     if not isinstance(value, distribution.Distribution): |  | 
| 404       raise errors.MonitoringInvalidValueTypeError(self._name, value) |  | 
| 405 |  | 
| 406     self._set_and_send_value(value, fields) |  | 
| 407 |  | 
| 408 |  | 
| 409 class CumulativeDistributionMetric(DistributionMetric): |  | 
| 410   """A DistributionMetric with is_cumulative set to True.""" |  | 
| 411 |  | 
| 412   def __init__( |  | 
| 413       self, name, bucketer=None, target=None, fields=None, time_fn=time.time): |  | 
| 414     super(CumulativeDistributionMetric, self).__init__( |  | 
| 415         name, |  | 
| 416         is_cumulative=True, |  | 
| 417         bucketer=bucketer, |  | 
| 418         target=target, |  | 
| 419         fields=fields, |  | 
| 420         time_fn=time_fn) |  | 
| 421 |  | 
| 422 |  | 
| 423 class NonCumulativeDistributionMetric(DistributionMetric): |  | 
| 424   """A DistributionMetric with is_cumulative set to False.""" |  | 
| 425 |  | 
| 426   def __init__( |  | 
| 427       self, name, bucketer=None, target=None, fields=None, time_fn=time.time): |  | 
| 428     super(NonCumulativeDistributionMetric, self).__init__( |  | 
| 429         name, |  | 
| 430         is_cumulative=False, |  | 
| 431         bucketer=bucketer, |  | 
| 432         target=target, |  | 
| 433         fields=fields, |  | 
| 434         time_fn=time_fn) |  | 
| OLD | NEW | 
|---|