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

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

Issue 1260293009: make version of ts_mon compatible with appengine (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: add noncululativedistribution metric to ts_mon imports Created 5 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
« no previous file with comments | « infra_libs/ts_mon/interface.py ('k') | infra_libs/ts_mon/monitors.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
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)
OLDNEW
« no previous file with comments | « infra_libs/ts_mon/interface.py ('k') | infra_libs/ts_mon/monitors.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698