| OLD | NEW |
| 1 # Copyright (c) 2015 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2015 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import collections | 5 import collections |
| 6 import copy | 6 import copy |
| 7 import json | 7 import json |
| 8 import logging | 8 import logging |
| 9 import os | 9 import os |
| 10 import time | 10 import time |
| (...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 154 """Poll a file instead of an endpoint. | 154 """Poll a file instead of an endpoint. |
| 155 | 155 |
| 156 The base_url in __init__ must be the file path. The file is assumed | 156 The base_url in __init__ must be the file path. The file is assumed |
| 157 to contain JSON objects, one per line. | 157 to contain JSON objects, one per line. |
| 158 | 158 |
| 159 The poller will first rotate the file (rename), read it, and delete | 159 The poller will first rotate the file (rename), read it, and delete |
| 160 the file. The writer of the file is expected to create a new file if | 160 the file. The writer of the file is expected to create a new file if |
| 161 it was rotated or deleted. | 161 it was rotated or deleted. |
| 162 """ | 162 """ |
| 163 endpoint = 'FILE' | 163 endpoint = 'FILE' |
| 164 field_keys = ('builder', 'slave', 'result', 'project_id', 'subproject_tag') | 164 build_field_keys = ('builder', 'slave', 'result', |
| 165 'project_id', 'subproject_tag') |
| 166 step_field_keys = ('builder', 'slave', 'step_result', |
| 167 'project_id', 'subproject_tag') |
| 168 |
| 169 ### These metrics are sent when a build finishes. |
| 165 result_count = ts_mon.CounterMetric('buildbot/master/builders/results/count', | 170 result_count = ts_mon.CounterMetric('buildbot/master/builders/results/count', |
| 166 description='Number of items consumed from ts_mon.log by mastermon') | 171 description='Number of items consumed from ts_mon.log by mastermon') |
| 167 # A custom bucketer with 12% resolution in the range of 1..10**5, | 172 # A custom bucketer with 12% resolution in the range of 1..10**5, |
| 168 # better suited for build cycle times. | 173 # better suited for build cycle times. |
| 169 bucketer = ts_mon.GeometricBucketer( | 174 bucketer = ts_mon.GeometricBucketer( |
| 170 growth_factor=10**0.05, num_finite_buckets=100) | 175 growth_factor=10**0.05, num_finite_buckets=100) |
| 171 cycle_times = ts_mon.CumulativeDistributionMetric( | 176 cycle_times = ts_mon.CumulativeDistributionMetric( |
| 172 'buildbot/master/builders/builds/durations', bucketer=bucketer, | 177 'buildbot/master/builders/builds/durations', bucketer=bucketer, |
| 173 description='Durations (in seconds) that slaves spent actively doing ' | 178 description='Durations (in seconds) that slaves spent actively doing ' |
| 174 'work towards builds for each builder') | 179 'work towards builds for each builder') |
| 175 pending_times = ts_mon.CumulativeDistributionMetric( | 180 pending_times = ts_mon.CumulativeDistributionMetric( |
| 176 'buildbot/master/builders/builds/pending_durations', bucketer=bucketer, | 181 'buildbot/master/builders/builds/pending_durations', bucketer=bucketer, |
| 177 description='Durations (in seconds) that the master spent waiting for ' | 182 description='Durations (in seconds) that the master spent waiting for ' |
| 178 'slaves to become available for each builder') | 183 'slaves to become available for each builder') |
| 179 total_times = ts_mon.CumulativeDistributionMetric( | 184 total_times = ts_mon.CumulativeDistributionMetric( |
| 180 'buildbot/master/builders/builds/total_durations', bucketer=bucketer, | 185 'buildbot/master/builders/builds/total_durations', bucketer=bucketer, |
| 181 description='Total duration (in seconds) that builds took to complete ' | 186 description='Total duration (in seconds) that builds took to complete ' |
| 182 'for each builder') | 187 'for each builder') |
| 183 | 188 |
| 184 pre_test_times = ts_mon.CumulativeDistributionMetric( | 189 pre_test_times = ts_mon.CumulativeDistributionMetric( |
| 185 'buildbot/master/builders/builds/pre_test_durations', bucketer=bucketer, | 190 'buildbot/master/builders/builds/pre_test_durations', bucketer=bucketer, |
| 186 description='Durations (in seconds) that builds spent before their ' | 191 description='Durations (in seconds) that builds spent before their ' |
| 187 '"before_tests" step') | 192 '"before_tests" step') |
| 188 | 193 |
| 194 ### This metric is sent when a step finishes. |
| 195 step_results_count = ts_mon.CounterMetric( |
| 196 'buildbot/master/builders/steps/results/count', |
| 197 description='Count of step results, per builder') |
| 198 |
| 189 def poll(self): | 199 def poll(self): |
| 190 LOGGER.info('Collecting results from %s', self._url) | 200 LOGGER.info('Collecting results from %s', self._url) |
| 191 | 201 |
| 192 if not os.path.isfile(self._url): | 202 if not os.path.isfile(self._url): |
| 193 LOGGER.info('No file found, assuming no data: %s', self._url) | 203 LOGGER.info('No file found, assuming no data: %s', self._url) |
| 194 return True | 204 return True |
| 195 | 205 |
| 196 try: | 206 try: |
| 197 rotated_name = rotated_filename(self._url) | 207 rotated_name = rotated_filename(self._url) |
| 198 # Remove the previous rotated file. We keep it on disk after | 208 # Remove the previous rotated file. We keep it on disk after |
| 199 # processing for debugging. | 209 # processing for debugging. |
| 200 safe_remove(rotated_name) | 210 safe_remove(rotated_name) |
| 201 os.rename(self._url, rotated_name) | 211 os.rename(self._url, rotated_name) |
| 202 with open(rotated_name, 'r') as f: | 212 with open(rotated_name, 'r') as f: |
| 203 for line in f: # pragma: no branch | 213 for line in f: # pragma: no branch |
| 204 self.handle_response(json.loads(line)) | 214 self.handle_response(json.loads(line)) |
| 205 except (ValueError, OSError, IOError) as e: | 215 except (ValueError, OSError, IOError) as e: |
| 206 LOGGER.error('Could not collect or send results from %s: %s', | 216 LOGGER.error('Could not collect or send results from %s: %s', |
| 207 self._url, e) | 217 self._url, e) |
| 208 | 218 |
| 209 # Never return False - we don't know if master is down. | 219 # Never return False - we don't know if master is down. |
| 210 return True | 220 return True |
| 211 | 221 |
| 212 def handle_response(self, data): | 222 def handle_response(self, data): |
| 213 fields = self.fields({k: data.get(k, 'unknown') for k in self.field_keys}) | 223 # We handle two cases here: whether the data was generated when a build |
| 214 self.result_count.increment(fields) | 224 # finished or when a step finished. We use the content of the json dict to |
| 215 if 'duration_s' in data: | 225 # tell the difference. |
| 216 self.cycle_times.add(data['duration_s'], fields) | 226 |
| 217 if 'pending_s' in data: | 227 if 'step_result' in data: # generated when a step finishes |
| 218 self.pending_times.add(data['pending_s'], fields) | 228 fields = self.fields({k: data.get(k, 'unknown') |
| 219 if 'total_s' in data: | 229 for k in self.step_field_keys}) |
| 220 self.total_times.add(data['total_s'], fields) | 230 self.step_results_count.increment(fields=fields) |
| 221 if 'pre_test_time_s' in data: | 231 |
| 222 self.pre_test_times.add(data['pre_test_time_s'], fields) | 232 else: # otherwise it's generated after a build finishes |
| 233 fields = self.fields({k: data.get(k, 'unknown') |
| 234 for k in self.build_field_keys}) |
| 235 self.result_count.increment(fields) |
| 236 if 'duration_s' in data: |
| 237 self.cycle_times.add(data['duration_s'], fields) |
| 238 if 'pending_s' in data: |
| 239 self.pending_times.add(data['pending_s'], fields) |
| 240 if 'total_s' in data: |
| 241 self.total_times.add(data['total_s'], fields) |
| 242 if 'pre_test_time_s' in data: |
| 243 self.pre_test_times.add(data['pre_test_time_s'], fields) |
| OLD | NEW |