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

Side by Side Diff: infra_libs/ts_mon/common/metrics.py

Issue 2213143002: Add infra_libs as a bootstrap dependency. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Removed the ugly import hack Created 4 years, 4 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 unified diff | Download patch
OLDNEW
(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 import copy
8
9 from infra_libs.ts_mon.protos import metrics_pb2
10
11 from infra_libs.ts_mon.common import distribution
12 from infra_libs.ts_mon.common import errors
13 from infra_libs.ts_mon.common import interface
14
15
16 MICROSECONDS_PER_SECOND = 1000000
17
18
19 class Metric(object):
20 """Abstract base class for a metric.
21
22 A Metric is an attribute that may be monitored across many targets. Examples
23 include disk usage or the number of requests a server has received. A single
24 process may keep track of many metrics.
25
26 Note that Metric objects may be initialized at any time (for example, at the
27 top of a library), but cannot be sent until the underlying Monitor object
28 has been set up (usually by the top-level process parsing the command line).
29
30 A Metric can actually store multiple values that are identified by a set of
31 fields (which are themselves key-value pairs). Fields can be passed to the
32 set() or increment() methods to modify a particular value, or passed to the
33 constructor in which case they will be used as the defaults for this Metric.
34
35 The unit of measurement for Metric data can be specified with MetricsDataUnits
36 when a Metric object is created:
37 e.g., MetricsDataUnits.SECONDS, MetricsDataUnits.BYTES, and etc..,
38 A full list of supported units can be found in the following protobuf file
39 : infra_libs/ts_mon/protos/metrics.proto
40
41 Do not directly instantiate an object of this class.
42 Use the concrete child classes instead:
43 * StringMetric for metrics with string value
44 * BooleanMetric for metrics with boolean values
45 * CounterMetric for metrics with monotonically increasing integer values
46 * GaugeMetric for metrics with arbitrarily varying integer values
47 * CumulativeMetric for metrics with monotonically increasing float values
48 * FloatMetric for metrics with arbitrarily varying float values
49
50 See http://go/inframon-doc for help designing and using your metrics.
51 """
52
53 def __init__(self, name, fields=None, description=None, units=None):
54 """Create an instance of a Metric.
55
56 Args:
57 name (str): the file-like name of this metric
58 fields (dict): a set of key-value pairs to be set as default metric fields
59 description (string): help string for the metric. Should be enough to
60 know what the metric is about.
61 units (int): the unit used to measure data for given
62 metric. Please use the attributes of MetricDataUnit to find
63 valid integer values for this argument.
64 """
65 self._name = name.lstrip('/')
66 self._start_time = None
67 fields = fields or {}
68 if len(fields) > 7:
69 raise errors.MonitoringTooManyFieldsError(self._name, fields)
70 self._fields = fields
71 self._normalized_fields = self._normalize_fields(self._fields)
72 self._description = description
73 self._units = units
74
75 interface.register(self)
76
77 @property
78 def name(self):
79 return self._name
80
81 @property
82 def start_time(self):
83 return self._start_time
84
85 def is_cumulative(self):
86 raise NotImplementedError()
87
88 def __eq__(self, other):
89 return (self.name == other.name and
90 self._fields == other._fields and
91 type(self) == type(other))
92
93 def unregister(self):
94 interface.unregister(self)
95
96 def serialize_to(self, collection_pb, start_time, fields, value, target):
97 """Generate metrics_pb2.MetricsData messages for this metric.
98
99 Args:
100 collection_pb (metrics_pb2.MetricsCollection): protocol buffer into which
101 to add the current metric values.
102 start_time (int): timestamp in microseconds since UNIX epoch.
103 target (Target): a Target to use.
104 """
105
106 metric_pb = collection_pb.data.add()
107 metric_pb.metric_name_prefix = interface.state.metric_name_prefix
108 metric_pb.name = self._name
109 if self._description is not None:
110 metric_pb.description = self._description
111 if self._units is not None:
112 metric_pb.units = self._units
113
114 self._populate_value(metric_pb, value, start_time)
115 self._populate_fields(metric_pb, fields)
116
117 target._populate_target_pb(metric_pb)
118
119 def _populate_fields(self, metric, fields):
120 """Fill in the fields attribute of a metric protocol buffer.
121
122 Args:
123 metric (metrics_pb2.MetricsData): a metrics protobuf to populate
124 fields (list of (key, value) tuples): normalized metric fields
125
126 Raises:
127 MonitoringInvalidFieldTypeError: if a field has a value of unknown type
128 """
129 for key, value in fields:
130 field = metric.fields.add()
131 field.name = key
132 if isinstance(value, basestring):
133 field.type = metrics_pb2.MetricsField.STRING
134 field.string_value = value
135 elif isinstance(value, bool):
136 field.type = metrics_pb2.MetricsField.BOOL
137 field.bool_value = value
138 elif isinstance(value, int):
139 field.type = metrics_pb2.MetricsField.INT
140 field.int_value = value
141 else:
142 raise errors.MonitoringInvalidFieldTypeError(self._name, key, value)
143
144 def _normalize_fields(self, fields):
145 """Merges the fields with the default fields and returns something hashable.
146
147 Args:
148 fields (dict): A dict of fields passed by the user, or None.
149
150 Returns:
151 A tuple of (key, value) tuples, ordered by key. This whole tuple is used
152 as the key in the self._values dict to identify the cell for a value.
153
154 Raises:
155 MonitoringTooManyFieldsError: if there are more than seven metric fields
156 """
157 if fields is None:
158 return self._normalized_fields
159
160 all_fields = copy.copy(self._fields)
161 all_fields.update(fields)
162
163 if len(all_fields) > 7:
164 raise errors.MonitoringTooManyFieldsError(self._name, all_fields)
165
166 return tuple(sorted(all_fields.iteritems()))
167
168 def _populate_value(self, metric, value, start_time):
169 """Fill in the the data values of a metric protocol buffer.
170
171 Args:
172 metric (metrics_pb2.MetricsData): a metrics protobuf to populate
173 value (see concrete class): the value of the metric to be set
174 start_time (int): timestamp in microseconds since UNIX epoch.
175 """
176 raise NotImplementedError()
177
178 def set(self, value, fields=None, target_fields=None):
179 """Set a new value for this metric. Results in sending a new value.
180
181 The subclass should do appropriate type checking on value and then call
182 self._set_and_send_value.
183
184 Args:
185 value (see concrete class): the value of the metric to be set
186 fields (dict): additional metric fields to complement those on self
187 target_fields (dict): overwrite some of the default target fields
188 """
189 raise NotImplementedError()
190
191 def get(self, fields=None, target_fields=None):
192 """Returns the current value for this metric.
193
194 Subclasses should never use this to get a value, modify it and set it again.
195 Instead use _incr with a modify_fn.
196 """
197 return interface.state.store.get(
198 self.name, self._normalize_fields(fields), target_fields)
199
200 def get_all(self):
201 return interface.state.store.iter_field_values(self.name)
202
203 def reset(self):
204 """Clears the values of this metric. Useful in unit tests.
205
206 It might be easier to call ts_mon.reset_for_unittest() in your setUp()
207 method instead of resetting every individual metric.
208 """
209
210 interface.state.store.reset_for_unittest(self.name)
211
212 def _set(self, fields, target_fields, value, enforce_ge=False):
213 interface.state.store.set(self.name, self._normalize_fields(fields),
214 target_fields, value, enforce_ge=enforce_ge)
215
216 def _incr(self, fields, target_fields, delta, modify_fn=None):
217 interface.state.store.incr(self.name, self._normalize_fields(fields),
218 target_fields, delta, modify_fn=modify_fn)
219
220
221 class StringMetric(Metric):
222 """A metric whose value type is a string."""
223
224 def _populate_value(self, metric, value, start_time):
225 metric.string_value = value
226
227 def set(self, value, fields=None, target_fields=None):
228 if not isinstance(value, basestring):
229 raise errors.MonitoringInvalidValueTypeError(self._name, value)
230 self._set(fields, target_fields, value)
231
232 def is_cumulative(self):
233 return False
234
235
236 class BooleanMetric(Metric):
237 """A metric whose value type is a boolean."""
238
239 def _populate_value(self, metric, value, start_time):
240 metric.boolean_value = value
241
242 def set(self, value, fields=None, target_fields=None):
243 if not isinstance(value, bool):
244 raise errors.MonitoringInvalidValueTypeError(self._name, value)
245 self._set(fields, target_fields, value)
246
247 def is_cumulative(self):
248 return False
249
250
251 class NumericMetric(Metric): # pylint: disable=abstract-method
252 """Abstract base class for numeric (int or float) metrics."""
253 # TODO(agable): Figure out if there's a way to send units with these metrics.
254
255 def increment(self, fields=None, target_fields=None):
256 self._incr(fields, target_fields, 1)
257
258 def increment_by(self, step, fields=None, target_fields=None):
259 self._incr(fields, target_fields, step)
260
261
262 class CounterMetric(NumericMetric):
263 """A metric whose value type is a monotonically increasing integer."""
264
265 def __init__(self, name, fields=None, start_time=None, description=None,
266 units=None):
267 super(CounterMetric, self).__init__(
268 name, fields=fields, description=description, units=units)
269 self._start_time = start_time
270
271 def _populate_value(self, metric, value, start_time):
272 metric.counter = value
273 metric.start_timestamp_us = int(start_time * MICROSECONDS_PER_SECOND)
274
275 def set(self, value, fields=None, target_fields=None):
276 if not isinstance(value, (int, long)):
277 raise errors.MonitoringInvalidValueTypeError(self._name, value)
278 self._set(fields, target_fields, value, enforce_ge=True)
279
280 def increment_by(self, step, fields=None, target_fields=None):
281 if not isinstance(step, (int, long)):
282 raise errors.MonitoringInvalidValueTypeError(self._name, step)
283 self._incr(fields, target_fields, step)
284
285 def is_cumulative(self):
286 return True
287
288
289 class GaugeMetric(NumericMetric):
290 """A metric whose value type is an integer."""
291
292 def _populate_value(self, metric, value, start_time):
293 metric.gauge = value
294
295 def set(self, value, fields=None, target_fields=None):
296 if not isinstance(value, (int, long)):
297 raise errors.MonitoringInvalidValueTypeError(self._name, value)
298 self._set(fields, target_fields, value)
299
300 def is_cumulative(self):
301 return False
302
303
304 class CumulativeMetric(NumericMetric):
305 """A metric whose value type is a monotonically increasing float."""
306
307 def __init__(self, name, fields=None, start_time=None, description=None,
308 units=None):
309 super(CumulativeMetric, self).__init__(
310 name, fields=fields, description=description, units=units)
311 self._start_time = start_time
312
313 def _populate_value(self, metric, value, start_time):
314 metric.cumulative_double_value = value
315 metric.start_timestamp_us = int(start_time * MICROSECONDS_PER_SECOND)
316
317 def set(self, value, fields=None, target_fields=None):
318 if not isinstance(value, (float, int)):
319 raise errors.MonitoringInvalidValueTypeError(self._name, value)
320 self._set(fields, target_fields, float(value), enforce_ge=True)
321
322 def is_cumulative(self):
323 return True
324
325
326 class FloatMetric(NumericMetric):
327 """A metric whose value type is a float."""
328
329 def _populate_value(self, metric, value, start_time):
330 metric.noncumulative_double_value = value
331
332 def set(self, value, fields=None, target_fields=None):
333 if not isinstance(value, (float, int)):
334 raise errors.MonitoringInvalidValueTypeError(self._name, value)
335 self._set(fields, target_fields, float(value))
336
337 def is_cumulative(self):
338 return False
339
340
341 class DistributionMetric(Metric):
342 """A metric that holds a distribution of values.
343
344 By default buckets are chosen from a geometric progression, each bucket being
345 approximately 1.59 times bigger than the last. In practice this is suitable
346 for many kinds of data, but you may want to provide a FixedWidthBucketer or
347 GeometricBucketer with different parameters."""
348
349 CANONICAL_SPEC_TYPES = {
350 2: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_2,
351 10**0.2: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_10_P_0_2,
352 10: metrics_pb2.PrecomputedDistribution.CANONICAL_POWERS_OF_10,
353 }
354
355 def __init__(self, name, is_cumulative=True, bucketer=None, fields=None,
356 start_time=None, description=None, units=None):
357 super(DistributionMetric, self).__init__(
358 name, fields=fields, description=description, units=units)
359 self._start_time = start_time
360
361 if bucketer is None:
362 bucketer = distribution.GeometricBucketer()
363
364 self._is_cumulative = is_cumulative
365 self.bucketer = bucketer
366
367 def _populate_value(self, metric, value, start_time):
368 pb = metric.distribution
369
370 pb.is_cumulative = self._is_cumulative
371 if self._is_cumulative:
372 metric.start_timestamp_us = int(start_time * MICROSECONDS_PER_SECOND)
373
374 # Copy the bucketer params.
375 if (value.bucketer.width == 0 and
376 value.bucketer.growth_factor in self.CANONICAL_SPEC_TYPES):
377 pb.spec_type = self.CANONICAL_SPEC_TYPES[value.bucketer.growth_factor]
378 else:
379 pb.spec_type = metrics_pb2.PrecomputedDistribution.CUSTOM_PARAMETERIZED
380 pb.width = value.bucketer.width
381 pb.growth_factor = value.bucketer.growth_factor
382 pb.num_buckets = value.bucketer.num_finite_buckets
383
384 # Copy the distribution bucket values. Only include the finite buckets, not
385 # the overflow buckets on each end.
386 pb.bucket.extend(self._running_zero_generator(
387 value.buckets.get(i, 0) for i in
388 xrange(1, value.bucketer.total_buckets - 1)))
389
390 # Add the overflow buckets if present.
391 if value.bucketer.underflow_bucket in value.buckets:
392 pb.underflow = value.buckets[value.bucketer.underflow_bucket]
393 if value.bucketer.overflow_bucket in value.buckets:
394 pb.overflow = value.buckets[value.bucketer.overflow_bucket]
395
396 if value.count != 0:
397 pb.mean = float(value.sum) / value.count
398
399 @staticmethod
400 def _running_zero_generator(iterable):
401 """Compresses sequences of zeroes in the iterable into negative zero counts.
402
403 For example an input of [1, 0, 0, 0, 2] is converted to [1, -3, 2].
404 """
405
406 count = 0
407
408 for value in iterable:
409 if value == 0:
410 count += 1
411 else:
412 if count != 0:
413 yield -count
414 count = 0
415 yield value
416
417 def add(self, value, fields=None, target_fields=None):
418 def modify_fn(dist, value):
419 if dist == 0:
420 dist = distribution.Distribution(self.bucketer)
421 dist.add(value)
422 return dist
423
424 self._incr(fields, target_fields, value, modify_fn=modify_fn)
425
426 def set(self, value, fields=None, target_fields=None):
427 """Replaces the distribution with the given fields with another one.
428
429 This only makes sense on non-cumulative DistributionMetrics.
430
431 Args:
432 value: A infra_libs.ts_mon.Distribution.
433 """
434
435 if self._is_cumulative:
436 raise TypeError(
437 'Cannot set() a cumulative DistributionMetric (use add() instead)')
438
439 if not isinstance(value, distribution.Distribution):
440 raise errors.MonitoringInvalidValueTypeError(self._name, value)
441
442 self._set(fields, target_fields, value)
443
444 def is_cumulative(self):
445 raise NotImplementedError() # Keep this class abstract.
446
447
448 class CumulativeDistributionMetric(DistributionMetric):
449 """A DistributionMetric with is_cumulative set to True."""
450
451 def __init__(self, name, bucketer=None, fields=None,
452 description=None, units=None):
453 super(CumulativeDistributionMetric, self).__init__(
454 name,
455 is_cumulative=True,
456 bucketer=bucketer,
457 fields=fields,
458 description=description,
459 units=units)
460
461 def is_cumulative(self):
462 return True
463
464
465 class NonCumulativeDistributionMetric(DistributionMetric):
466 """A DistributionMetric with is_cumulative set to False."""
467
468 def __init__(self, name, bucketer=None, fields=None,
469 description=None, units=None):
470 super(NonCumulativeDistributionMetric, self).__init__(
471 name,
472 is_cumulative=False,
473 bucketer=bucketer,
474 fields=fields,
475 description=description,
476 units=units)
477
478 def is_cumulative(self):
479 return False
480
481
482 class MetaMetricsDataUnits(type):
483 """Metaclass to populate the enum values of metrics_pb2.MetricsData.Units."""
484 def __new__(mcs, name, bases, attrs):
485 attrs.update(metrics_pb2.MetricsData.Units.items())
486 return super(MetaMetricsDataUnits, mcs).__new__(mcs, name, bases, attrs)
487
488
489 class MetricsDataUnits(object):
490 """An enumeration class for units of measurement for Metrics data.
491 See infra_libs/ts_mon/protos/metrics.proto for a full list of supported units.
492 """
493 __metaclass__ = MetaMetricsDataUnits
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698