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 |