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 pre_collect(self):
54        """
55        Hook called before metrics are being collected.
56        """
57        pass
58
59    def _store_sample(self, sample):
60        self._samples.append(sample)
61
62    def collect_metric(self):
63        """
64        Collects one sample.
65
66        Implementations should call self._store_sample() once if it's not an
67        aggregate, i.e., it overrides self._aggregate().
68        """
69        pass
70
71    @classmethod
72    def from_metric(cls, other):
73        """
74        Instantiate from an existing metric instance.
75        """
76        metric = cls(
77                description=other.description,
78                units=other.units,
79                higher_is_better=other.higher_is_better)
80        metric.values = other.values
81        return metric
82
83class PeakMetric(Metric):
84    """
85    Metric that collects the peak of another metric.
86    """
87
88    @property
89    def description(self):
90        return 'peak_' + super(PeakMetric, self).description
91
92    def _aggregate(self, samples):
93        return max(samples)
94
95class SumMetric(Metric):
96    """
97    Metric that sums another metric.
98    """
99
100    @property
101    def description(self):
102        return 'sum_' + super(SumMetric, self).description
103
104    def _aggregate(self, samples):
105        return sum(samples)
106
107class MemUsageMetric(Metric):
108    """
109    Metric that collects memory usage in percent.
110
111    Memory usage is collected in percent. Buffers and cached are calculated
112    as free memory.
113    """
114    def __init__(self, system_facade):
115        super(MemUsageMetric, self).__init__('memory_usage', units='percent')
116        self.system_facade = system_facade
117
118    def collect_metric(self):
119        total_memory = self.system_facade.get_mem_total()
120        free_memory = self.system_facade.get_mem_free_plus_buffers_and_cached()
121        used_memory = total_memory - free_memory
122        usage_percent = (used_memory * 100) / total_memory
123        self._store_sample(usage_percent)
124
125class CpuUsageMetric(Metric):
126    """
127    Metric that collects cpu usage in percent.
128    """
129    def __init__(self, system_facade):
130        super(CpuUsageMetric, self).__init__('cpu_usage', units='percent')
131        self.last_usage = None
132        self.system_facade = system_facade
133
134    def pre_collect(self):
135        self.last_usage = self.system_facade.get_cpu_usage()
136
137    def collect_metric(self):
138        """
139        Collects CPU usage in percent.
140        """
141        current_usage = self.system_facade.get_cpu_usage()
142        # Compute the percent of active time since the last update to
143        # current_usage.
144        usage_percent = 100 * self.system_facade.compute_active_cpu_time(
145                self.last_usage, current_usage)
146        self._store_sample(usage_percent)
147        self.last_usage = current_usage
148
149class AllocatedFileHandlesMetric(Metric):
150    """
151    Metric that collects the number of allocated file handles.
152    """
153    def __init__(self, system_facade):
154        super(AllocatedFileHandlesMetric, self).__init__(
155                'allocated_file_handles', units='handles')
156        self.system_facade = system_facade
157
158    def collect_metric(self):
159        self._store_sample(self.system_facade.get_num_allocated_file_handles())
160
161class StorageWrittenMetric(Metric):
162    """
163    Metric that collects amount of data written to persistent storage.
164    """
165    def __init__(self, system_facade):
166        super(StorageWrittenMetric, self).__init__(
167                'storage_written', units='kB')
168        self.last_written_kb = None
169        self.system_facade = system_facade
170
171    def pre_collect(self):
172        statistics = self.system_facade.get_storage_statistics()
173        self.last_written_kb = statistics['written_kb']
174
175    def collect_metric(self):
176        """
177        Collects total amount of data written to persistent storage in kB.
178        """
179        statistics = self.system_facade.get_storage_statistics()
180        written_kb = statistics['written_kb']
181        written_period = written_kb - self.last_written_kb
182        self._store_sample(written_period)
183        self.last_written_kb = written_kb
184
185class TemperatureMetric(Metric):
186    """
187    Metric that collects the max of the temperatures measured on all sensors.
188    """
189    def __init__(self, system_facade):
190        super(TemperatureMetric, self).__init__('temperature', units='Celsius')
191        self.system_facade = system_facade
192
193    def collect_metric(self):
194        self._store_sample(self.system_facade.get_current_temperature_max())
195
196def create_default_metric_set(system_facade):
197    """
198    Creates the default set of metrics.
199
200    @param system_facade the system facade to initialize the metrics with.
201    @return a list with Metric instances.
202    """
203    cpu = CpuUsageMetric(system_facade)
204    mem = MemUsageMetric(system_facade)
205    file_handles = AllocatedFileHandlesMetric(system_facade)
206    storage_written = StorageWrittenMetric(system_facade)
207    temperature = TemperatureMetric(system_facade)
208    peak_cpu = PeakMetric.from_metric(cpu)
209    peak_mem = PeakMetric.from_metric(mem)
210    peak_temperature = PeakMetric.from_metric(temperature)
211    sum_storage_written = SumMetric.from_metric(storage_written)
212    return [cpu,
213            mem,
214            file_handles,
215            storage_written,
216            temperature,
217            peak_cpu,
218            peak_mem,
219            peak_temperature,
220            sum_storage_written]
221
222class SystemMetricsCollector(object):
223    """
224    Collects system metrics.
225    """
226    def __init__(self, system_facade, metrics = None):
227        """
228        Initialize with facade and metric classes.
229
230        @param system_facade The system facade to use for querying the system,
231                e.g. system_facade_native.SystemFacadeNative for client tests.
232        @param metrics List of metric instances. If None, the default set will
233                be created.
234        """
235        self.metrics = (create_default_metric_set(system_facade)
236                        if metrics is None else metrics)
237
238    def pre_collect(self):
239        """
240        Calls pre hook of metrics.
241        """
242        for metric in self.metrics:
243            metric.pre_collect()
244
245    def collect_snapshot(self):
246        """
247        Collects one snapshot of metrics.
248        """
249        for metric in self.metrics:
250            metric.collect_metric()
251
252    def write_metrics(self, writer_function):
253        """
254        Writes the collected metrics using the specified writer function.
255
256        @param writer_function: A function with the following signature:
257                 f(description, value, units, higher_is_better)
258        """
259        for metric in self.metrics:
260            writer_function(
261                    description=metric.description,
262                    value=metric.values,
263                    units=metric.units,
264                    higher_is_better=metric.higher_is_better)
265