blob: b721a307740f5fd2d8b62317c273f20044e15919 [file] [log] [blame]
Xinan Linc9f01152020-02-05 22:05:13 -08001# Copyright 2020 The Chromium OS 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"""Module for collecting and uploading analytics metrics."""
6
7import json
8import logging
9import re
10
11import constants
12import rest_client
13
14from chromite.api.gen.test_platform.suite_scheduler import analytics_pb2
15from chromite.api.gen.chromite.api import artifacts_pb2
16from chromite.api.gen.chromiumos import branch_pb2
17from chromite.api.gen.chromiumos import common_pb2
18
19from google.protobuf import timestamp_pb2
20from google.protobuf import json_format
21
22
23class AnalyticsError(Exception):
24 """Raised when there is a general error."""
25
26
27class AnalyticsBase(object):
28 """Base class to handle analytics data."""
29
30 def __init__(self, table, message):
31 """Initialize a base event.
32
33 Args:
34 table: string of the table name.
35 message: protobuf object to upload.
36 """
37 self.bq_client = _gen_bq_client(table)
38 self.message = message
39
40 def upload(self):
41 """Convey protobuf to json and insert to BQ."""
42 rows = [{
43 'json':
44 json.loads(
45 json_format.MessageToJson(
46 self.message,
47 preserving_proto_field_name=True,
48 including_default_value_fields=True))
49 }]
50 return self.bq_client.insert(rows)
51
52class ExecutionTask(AnalyticsBase):
53 """Track the execution for the queued tasks."""
54
55 def __init__(self, task_id):
Xinan Lin1e8e7912020-07-31 09:52:16 -070056 """Initialize an ExecutionTask.
Xinan Linc9f01152020-02-05 22:05:13 -080057
58 Args:
59 task_id: string of uuid, assigned by the event which created this task.
60 """
61 self.task = analytics_pb2.ExecutionTask(
62 queued_task_id=task_id,
63 request_sent=timestamp_pb2.Timestamp())
64 self.task.request_sent.GetCurrentTime()
65 super(ExecutionTask, self).__init__(constants.Metrics.EXECUTION_TASK_TABLE,
66 self.task)
67
68 def update_result(self, resp):
69 """Attach the buildbucket id to task_execution.
70
71 Args:
72 resp: build_pb2.Build object of Buildbucket response.
73 """
74 try:
75 if resp.id > 0:
76 self.task.response.ctp_build_id = str(resp.id)
77 return
78 # If id is absent, we should log resp as an error message.
79 logging.warning('No build id in buildbucket response %s.', str(resp))
80 except AttributeError as e:
81 logging.warning('Failed to parse the buildbucket response, %s.'
82 'Error: %s', str(resp), str(e))
83 self.task.error.error_message = str(resp)
84
85
86class ScheduleJobSection(AnalyticsBase):
87 """Record all the scheduling decisions in a job section."""
88
89 def __init__(self, task_info):
90 """Initialize a ScheduleJobSection
91
92 Args:
93 task_info: a config_reader.TaskInfo object.
94 """
95 self.job_section = _job_section(task_info)
96 self.bq_client = _gen_bq_client(constants.Metrics.TRIGGER_EVENT_TABLE)
97 super(ScheduleJobSection, self).__init__(
98 constants.Metrics.TRIGGER_EVENT_TABLE,
99 self.job_section)
100
101 def add_board(self, board):
102 """Add a board to job_section.
103
104 Boards added are the target, on which we schedule the configured
105 suite test.
106
107 Args:
108 board: a string of board name.
109 """
110 self.job_section.build_targets.add(name=board)
111
112 def add_model(self, model):
113 """Add a model to job_section.
114
115 Models added are the target, on which we schedule the configured
116 suite test. If not set in the config, all models under the boards in this
117 section will be appended.
118
119 Args:
120 model: a string of model name.
121 """
122 self.job_section.models.add(value=model)
123
124 def add_matched_build(self, board, build_type, milestone, manifest):
125 """Add an eligible build for this job section.
126
127 Args:
128 board: a string of board name.
129 build_type: a string of build type with the board, like 'release'.
130 milestone: a string milestone, like '80'.
131 manifest: a string of chromeos version, e.g. '12240.0.0'.
132 """
133 self.job_section.matched_builds.add(release_build=analytics_pb2.BuildInfo(
134 build_target=common_pb2.BuildTarget(name=board),
135 milestone=int(milestone),
136 chrome_os_version=manifest,
137 type=_branch_type(build_type)))
138
139 def add_matched_relax_build(self, board, build_type, milestone, manifest):
140 """Add an eligible relax build for this job section.
141
142 Args:
143 board: a string of board name.
144 build_type: a string of build type with the board, like 'release'.
145 milestone: a string milestone, like '80'.
146 manifest: a string of chromeos version, e.g. '12240.0.0'.
147 """
148 self.job_section.matched_builds.add(relax_build=analytics_pb2.BuildInfo(
149 build_target=common_pb2.BuildTarget(name=board),
150 milestone=int(milestone),
151 chrome_os_version=manifest,
152 type=_branch_type(build_type)))
153
154 def add_matched_fw_build(self, board, build_type, artifact, read_only=True):
155 """Add an eligible firmware build for this job section.
156
157 Args:
158 board: a string of board name.
159 build_type: a string of build type with the board, like 'release'.
160 artifact: relative path to the artifact file, e.g.
161 'firmware-board-12345.67.B-firmwarebranch/RFoo-1.0.0-b1e234567/board'.
162 read_only: a boolean if true for RO firmware build and false for RW firmware.
163 """
164 firmware_build = analytics_pb2.FirmwareBuildInfo(
165 build_target=common_pb2.BuildTarget(name=board),
166 artifact=artifacts_pb2.Artifact(path=artifact),
167 type=_branch_type(build_type))
168 if read_only:
169 self.job_section.matched_builds.add(
170 firmware_ro_build=firmware_build)
171 else:
172 self.job_section.matched_builds.add(
173 firmware_rw_build=firmware_build)
174
175 def add_schedule_job(self, board, model, msg=None, task_id=None):
176 """Add a schedule job.
177
178 Args:
179 board: a string of board name.
180 model: a string of model name.
181 msg: a string of the reason we drop this build.
182 task_id: a string of uuid to track it in taskqueue.
183
184 Raise:
185 AnalyticsError: if both msg/task_id are specified or both are None.
186 """
187 if all([msg, task_id]):
188 raise AnalyticsError('msg and task_id should not be set both; got'
189 'msg: %s, task_id: %s.' % (msg, task_id))
190 if not any([msg, task_id]):
191 raise AnalyticsError('At least one of msg and task_id should '
192 'be set; got msg: %s, task_id: %s.' % (msg, task_id))
193 new_job = self.job_section.schedule_jobs.add(
194 generated_time=timestamp_pb2.Timestamp())
195 if model:
196 new_job.model.value = model
197 new_job.build_target.name = board
198 if msg:
199 new_job.justification = msg
200 if task_id:
201 new_job.queued_task_id = task_id
202 new_job.generated_time.GetCurrentTime()
203
204
205def _gen_bq_client(table):
206 """A wrapper to generate Bigquery REST client.
207
208 Args:
209 table: string of the table name.
210
211 Returns:
212 REST client for Bigquery API.
213 """
214 project = constants.Metrics.PROJECT_ID_STAGING
215 if (constants.environment() == constants.RunningEnv.ENV_PROD and
216 constants.application_id() == constants.AppID.PROD_APP):
217 project = constants.Metrics.PROJECT_ID
218 return rest_client.BigqueryRestClient(
219 rest_client.BaseRestClient(
220 constants.RestClient.BIGQUERY_CLIENT.scopes,
221 constants.RestClient.BIGQUERY_CLIENT.service_name,
222 constants.RestClient.BIGQUERY_CLIENT.service_version),
223 project=project,
224 dataset=constants.Metrics.DATASET,
225 table=table)
226
227
228def _parse_branch_spec(spec):
229 """A wrapper to parse the branch spec into channcel and lag.
230
231 Args:
232 spec: string of branch spec, e.g. '>=tot-2', '==tot'
233
234 Returns:
235 channel: analytics_pb2.BranchFilter for the builder type.
236 lag: number of minor versions behind tip-of-tree.
237
238 Raise:
239 AnalyticsError: if the spec are not supported.
240 """
241 if not 'tot' in spec:
242 raise ValueError('Only support branch specs relative to tot, '
243 'e.g. >=tot-2, got %s' % spec)
244 if spec[:2] not in ["<=", ">=", "=="]:
245 raise ValueError('Only "<=", ">=", "==" are supported, '
246 'e.g. >=tot-2, got %s' % spec)
247 to_operator = {
248 "==": analytics_pb2.BranchFilter.EQ,
249 ">=": analytics_pb2.BranchFilter.GE,
250 "<=": analytics_pb2.BranchFilter.LE}
251 TOT_SPEC = r'.*tot-(?P<lag>.+)'
252 match = re.match(TOT_SPEC, spec)
253 if not match:
254 return analytics_pb2.BranchFilter.MASTER, to_operator[spec[:2]], 0
255 lag = match.group('lag')
256 if lag.isdigit():
257 return analytics_pb2.BranchFilter.MASTER, to_operator[spec[:2]], int(lag)
258 raise ValueError('Failed to get the lag number from spec, %s' % spec)
259
260
261def _job_section(task_info):
262 """A wrapper to generate job_section from task_info.
263
264 Args:
265 task_info: a config_reader.TaskInfo object.
266
267 Returns:
268 job_section: a analytics_pb2.ScheduleJobSection object.
269 """
270 job_section = analytics_pb2.ScheduleJobSection(
Xinan Lin3ffd97c2020-03-31 10:34:43 -0700271 job_name=task_info.name, pool=task_info.pool, suite=task_info.suite)
Xinan Linc9f01152020-02-05 22:05:13 -0800272 if task_info.hour:
273 job_section.schedule_job_trigger.nightly.hour = task_info.hour
274 elif task_info.day:
275 job_section.schedule_job_trigger.weekly.day = task_info.day
276 else:
277 job_section.schedule_job_trigger.interval.pause = 0
278 build_filters = job_section.schedule_job_trigger.build_filters
279 build_filters.only_hwtest_sanity_required = (
280 task_info.only_hwtest_sanity_required is not None)
281 if task_info.branch_specs:
282 for spec in task_info.branch_specs:
283 try:
284 channel, op, lag = _parse_branch_spec(spec)
285 build_filters.branch_filters.add(
286 channel=channel, operator=op, lag=lag)
287 except ValueError as e:
288 logging.warning('Failed to parse the spec, %s', str(e))
289 if task_info.firmware_rw_build_spec:
290 build_filters.firmware_rw_build_spec = branch_pb2.Branch.FIRMWARE
291 if task_info.firmware_ro_build_spec:
292 build_filters.firmware_ro_build_spec = branch_pb2.Branch.FIRMWARE
293 return job_section
294
295
296def _branch_type(build_type):
297 if build_type == "release":
298 return branch_pb2.Branch.RELEASE
299 if build_type == "firmware":
300 return branch_pb2.Branch.FIRMWARE
301 return branch_pb2.Branch.UNSPECIFIED