1class Metric(object):
2    """Abstract base class for metrics."""
3    def __init__(self,
4                 description,
5                 units=None,
6                 higher_is_better=False):
7        """
8        Initializes a Metric.
9        @param description: Description of the metric, e.g., used as label on a
10                dashboard chart
11        @param units: Units of the metric, e.g. percent, seconds, MB.
12        @param higher_is_better: Whether a higher value is considered better or
13                not.
14        """
15        self._description = description
16        self._units = units
17        self._higher_is_better = higher_is_better
18        self._samples = []
19
20    @property
21    def description(self):
22        """Description of the metric."""
23        return self._description
24
25    @property
26    def units(self):
27        """Units of the metric."""
28        return self._units
29
30    @property
31    def higher_is_better(self):
32        """Whether a higher value is considered better or not."""
33        return self._higher_is_better
34
35    @property
36    def values(self):
37        """Measured values of the metric."""
38        if len(self._samples) == 0:
39            return self._samples
40        return self._aggregate(self._samples)
41
42    @values.setter
43    def values(self, samples):
44        self._samples = samples
45
46    def _aggregate(self, samples):
47        """
48        Subclasses can override this to aggregate the metric into a single
49        sample.
50        """
51        return samples
52
53    def _store_sample(self, sample):
54        self._samples.append(sample)
55
56    def pre_collect(self):
57        """
58        Hook called before metrics are being collected.
59        """
60        pass
61
62    def post_collect(self):
63        """
64        Hook called after metrics has been collected.
65        """
66        pass
67
68    def collect_metric(self):
69        """
70        Collects one sample.
71
72        Implementations should call self._store_sample() once if it's not an
73        aggregate, i.e., it overrides self._aggregate().
74        """
75        pass
76
77    @classmethod
78    def from_metric(cls, other):
79        """
80        Instantiate from an existing metric instance.
81        """
82        metric = cls(
83                description=other.description,
84                units=other.units,
85                higher_is_better=other.higher_is_better)
86        metric.values = other.values
87        return metric
88
89class PeakMetric(Metric):
90    """
91    Metric that collects the peak of another metric.
92    """
93
94    @property
95    def description(self):
96        return 'peak_' + super(PeakMetric, self).description
97
98    def _aggregate(self, samples):
99        return max(samples)
100
101class SumMetric(Metric):
102    """
103    Metric that sums another metric.
104    """
105
106    @property
107    def description(self):
108        return 'sum_' + super(SumMetric, self).description
109
110    def _aggregate(self, samples):
111        return sum(samples)
112
113class MemUsageMetric(Metric):
114    """
115    Metric that collects memory usage in percent.
116
117    Memory usage is collected in percent. Buffers and cached are calculated
118    as free memory.
119    """
120    def __init__(self, system_facade):
121        super(MemUsageMetric, self).__init__('memory_usage', units='percent')
122        self.system_facade = system_facade
123
124    def collect_metric(self):
125        total_memory = self.system_facade.get_mem_total()
126        free_memory = self.system_facade.get_mem_free_plus_buffers_and_cached()
127        used_memory = total_memory - free_memory
128        usage_percent = (used_memory * 100) / total_memory
129        self._store_sample(usage_percent)
130
131class CpuUsageMetric(Metric):
132    """
133    Metric that collects cpu usage in percent.
134    """
135    def __init__(self, system_facade):
136        super(CpuUsageMetric, self).__init__('cpu_usage', units='percent')
137        self.last_usage = None
138        self.system_facade = system_facade
139
140    def pre_collect(self):
141        self.last_usage = self.system_facade.get_cpu_usage()
142
143    def collect_metric(self):
144        """
145        Collects CPU usage in percent.
146        """
147        current_usage = self.system_facade.get_cpu_usage()
148        # Compute the percent of active time since the last update to
149        # current_usage.
150        usage_percent = 100 * self.system_facade.compute_active_cpu_time(
151                self.last_usage, current_usage)
152        self._store_sample(usage_percent)
153        self.last_usage = current_usage
154
155class AllocatedFileHandlesMetric(Metric):
156    """
157    Metric that collects the number of allocated file handles.
158    """
159    def __init__(self, system_facade):
160        super(AllocatedFileHandlesMetric, self).__init__(
161                'allocated_file_handles', units='handles')
162        self.system_facade = system_facade
163
164    def collect_metric(self):
165        self._store_sample(self.system_facade.get_num_allocated_file_handles())
166
167class StorageWrittenAmountMetric(Metric):
168    """
169    Metric that collects amount of kB written to persistent storage.
170    """
171    def __init__(self, system_facade):
172        super(StorageWrittenAmountMetric, self).__init__(
173                'storage_written_amount', units='kB')
174        self.last_written_kb = None
175        self.system_facade = system_facade
176
177    def pre_collect(self):
178        statistics = self.system_facade.get_storage_statistics()
179        self.last_written_kb = statistics['written_kb']
180
181    def collect_metric(self):
182        """
183        Collects total amount of data written to persistent storage in kB.
184        """
185        statistics = self.system_facade.get_storage_statistics()
186        written_kb = statistics['written_kb']
187        written_period = written_kb - self.last_written_kb
188        self._store_sample(written_period)
189        self.last_written_kb = written_kb
190
191class StorageWrittenCountMetric(Metric):
192    """
193    Metric that collects the number of writes to persistent storage.
194    """
195    def __init__(self, system_facade):
196        super(StorageWrittenCountMetric, self).__init__(
197                'storage_written_count', units='count')
198        self.system_facade = system_facade
199
200    def pre_collect(self):
201        command = ('/usr/sbin/fatrace', '--timestamp', '--filter=W')
202        self.system_facade.start_bg_worker(command)
203
204    def collect_metric(self):
205        output = self.system_facade.get_and_discard_bg_worker_output()
206        # fatrace outputs a line of text for each file it detects being written
207        # to.
208        written_count = output.count('\n')
209        self._store_sample(written_count)
210
211    def post_collect(self):
212        self.system_facade.stop_bg_worker()
213
214class TemperatureMetric(Metric):
215    """
216    Metric that collects the max of the temperatures measured on all sensors.
217    """
218    def __init__(self, system_facade):
219        super(TemperatureMetric, self).__init__('temperature', units='Celsius')
220        self.system_facade = system_facade
221
222    def collect_metric(self):
223        self._store_sample(self.system_facade.get_current_temperature_max())
224
225def create_default_metric_set(system_facade):
226    """
227    Creates the default set of metrics.
228
229    @param system_facade the system facade to initialize the metrics with.
230    @return a list with Metric instances.
231    """
232    cpu = CpuUsageMetric(system_facade)
233    mem = MemUsageMetric(system_facade)
234    file_handles = AllocatedFileHandlesMetric(system_facade)
235    storage_written_amount = StorageWrittenAmountMetric(system_facade)
236    temperature = TemperatureMetric(system_facade)
237    peak_cpu = PeakMetric.from_metric(cpu)
238    peak_mem = PeakMetric.from_metric(mem)
239    peak_temperature = PeakMetric.from_metric(temperature)
240    sum_storage_written_amount = SumMetric.from_metric(storage_written_amount)
241    return [cpu,
242            mem,
243            file_handles,
244            storage_written_amount,
245            temperature,
246            peak_cpu,
247            peak_mem,
248            peak_temperature,
249            sum_storage_written_amount]
250
251class SystemMetricsCollector(object):
252    """
253    Collects system metrics.
254    """
255    def __init__(self, system_facade, metrics = None):
256        """
257        Initialize with facade and metric classes.
258
259        @param system_facade The system facade to use for querying the system,
260                e.g. system_facade_native.SystemFacadeNative for client tests.
261        @param metrics List of metric instances. If None, the default set will
262                be created.
263        """
264        self.metrics = (create_default_metric_set(system_facade)
265                        if metrics is None else metrics)
266
267    def pre_collect(self):
268        """
269        Calls pre hook of metrics.
270        """
271        for metric in self.metrics:
272            metric.pre_collect()
273
274    def post_collect(self):
275        """
276        Calls post hook of metrics.
277        """
278        for metric in self.metrics:
279            metric.post_collect()
280
281    def collect_snapshot(self):
282        """
283        Collects one snapshot of metrics.
284        """
285        for metric in self.metrics:
286            metric.collect_metric()
287
288    def write_metrics(self, writer_function):
289        """
290        Writes the collected metrics using the specified writer function.
291
292        @param writer_function: A function with the following signature:
293                 f(description, value, units, higher_is_better)
294        """
295        for metric in self.metrics:
296            writer_function(
297                    description=metric.description,
298                    value=metric.values,
299                    units=metric.units,
300                    higher_is_better=metric.higher_is_better)
301