1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.device.cloud;
17 
18 import com.android.tradefed.command.remote.DeviceDescriptor;
19 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
20 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
21 import com.android.tradefed.log.LogUtil.CLog;
22 import com.android.tradefed.result.LogDataType;
23 import com.android.tradefed.result.error.ErrorIdentifier;
24 import com.android.tradefed.result.error.InfraErrorIdentifier;
25 import com.android.tradefed.targetprep.TargetSetupError;
26 import com.android.tradefed.util.CommandResult;
27 import com.android.tradefed.util.CommandStatus;
28 
29 import com.google.common.annotations.VisibleForTesting;
30 import com.google.common.base.Strings;
31 import com.google.common.collect.ImmutableMap;
32 import com.google.common.net.HostAndPort;
33 
34 import org.json.JSONArray;
35 import org.json.JSONException;
36 import org.json.JSONObject;
37 
38 import java.io.File;
39 import java.io.IOException;
40 import java.nio.charset.StandardCharsets;
41 import java.nio.file.Files;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.HashMap;
45 import java.util.LinkedHashMap;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50 
51 /** Structure to hold relevant data for a given GCE AVD instance. */
52 public class GceAvdInfo {
53 
54     // Patterns to match from Oxygen client's return message to identify error.
55     private static final LinkedHashMap<InfraErrorIdentifier, String> OXYGEN_ERROR_PATTERN_MAP;
56 
57     static {
58         OXYGEN_ERROR_PATTERN_MAP = new LinkedHashMap<InfraErrorIdentifier, String>();
59         // Order the error message matching carefully so it can surface the expected error properly
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_SERVER_SHUTTING_DOWN, "server_shutting_down")60         OXYGEN_ERROR_PATTERN_MAP.put(
61                 InfraErrorIdentifier.OXYGEN_SERVER_SHUTTING_DOWN, "server_shutting_down");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_BAD_GATEWAY_ERROR, "UNAVAILABLE: HTTP status code 502")62         OXYGEN_ERROR_PATTERN_MAP.put(
63                 InfraErrorIdentifier.OXYGEN_BAD_GATEWAY_ERROR, "UNAVAILABLE: HTTP status code 502");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_REQUEST_TIMEOUT, "DeadlineExceeded")64         OXYGEN_ERROR_PATTERN_MAP.put(
65                 InfraErrorIdentifier.OXYGEN_REQUEST_TIMEOUT, "DeadlineExceeded");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_RESOURCE_EXHAUSTED, "ResourceExhausted")66         OXYGEN_ERROR_PATTERN_MAP.put(
67                 InfraErrorIdentifier.OXYGEN_RESOURCE_EXHAUSTED, "ResourceExhausted");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_NOT_ENOUGH_RESOURCE, "Oxygen currently doesn't have enough resources to fulfil this request")68         OXYGEN_ERROR_PATTERN_MAP.put(
69                 InfraErrorIdentifier.OXYGEN_NOT_ENOUGH_RESOURCE,
70                 "Oxygen currently doesn't have enough resources to fulfil this request");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_SERVER_CONNECTION_FAILURE, "Bad Gateway")71         OXYGEN_ERROR_PATTERN_MAP.put(
72                 InfraErrorIdentifier.OXYGEN_SERVER_CONNECTION_FAILURE, "Bad Gateway");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_CLIENT_LEASE_ERROR, "OxygenClient")73         OXYGEN_ERROR_PATTERN_MAP.put(
74                 InfraErrorIdentifier.OXYGEN_CLIENT_LEASE_ERROR, "OxygenClient");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_TIMEOUT, "Lease aborted due to launcher failure: Timed out waiting for virtual device to" + " start")75         OXYGEN_ERROR_PATTERN_MAP.put(
76                 InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_TIMEOUT,
77                 "Lease aborted due to launcher failure: Timed out waiting for virtual device to"
78                         + " start");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_FAILURE, "Lease aborted due to launcher failure")79         OXYGEN_ERROR_PATTERN_MAP.put(
80                 InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_FAILURE,
81                 "Lease aborted due to launcher failure");
OXYGEN_ERROR_PATTERN_MAP.put( InfraErrorIdentifier.OXYGEN_SERVER_LB_CONNECTION_ERROR, "desc = connection error")82         OXYGEN_ERROR_PATTERN_MAP.put(
83                 InfraErrorIdentifier.OXYGEN_SERVER_LB_CONNECTION_ERROR, "desc = connection error");
84     }
85 
86     // Error message for specify Oxygen error.
87     private static final ImmutableMap<InfraErrorIdentifier, String> OXYGEN_ERROR_MESSAGE_MAP =
88             ImmutableMap.of(
89                     InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_FAILURE,
90                             "AVD failed to boot up properly",
91                     InfraErrorIdentifier.OXYGEN_SERVER_SHUTTING_DOWN,
92                             "Unexpected error from Oxygen service",
93                     InfraErrorIdentifier.OXYGEN_BAD_GATEWAY_ERROR,
94                             "Unexpected error from Oxygen service",
95                     InfraErrorIdentifier.OXYGEN_REQUEST_TIMEOUT,
96                             "Unexpected error from Oxygen service. Request timed out.",
97                     InfraErrorIdentifier.OXYGEN_RESOURCE_EXHAUSTED,
98                             "Oxygen ran out of capacity to lease virtual device",
99                     InfraErrorIdentifier.OXYGEN_SERVER_CONNECTION_FAILURE,
100                             "Unexpected error from Oxygen service",
101                     InfraErrorIdentifier.OXYGEN_CLIENT_LEASE_ERROR,
102                             "Oxygen client failed to lease a device",
103                     InfraErrorIdentifier.OXYGEN_DEVICE_LAUNCHER_TIMEOUT, "AVD boot timed out");
104 
105     public static class LogFileEntry {
106         public final String path;
107         public final LogDataType type;
108         // The name is optional and defaults to an empty string.
109         public final String name;
110 
111         @VisibleForTesting
LogFileEntry(String path, LogDataType type, String name)112         LogFileEntry(String path, LogDataType type, String name) {
113             this.path = path;
114             this.type = type;
115             this.name = name;
116         }
117 
LogFileEntry(JSONObject log)118         LogFileEntry(JSONObject log) throws JSONException {
119             path = log.getString("path");
120             type = parseLogDataType(log.getString("type"));
121             name = log.optString("name", "");
122         }
123 
parseLogDataType(String typeString)124         private LogDataType parseLogDataType(String typeString) {
125             try {
126                 return LogDataType.valueOf(typeString);
127             } catch (IllegalArgumentException e) {
128                 CLog.w("Unknown log type in GCE AVD info: %s", typeString);
129                 return LogDataType.UNKNOWN;
130             }
131         }
132     }
133 
134     public static final List<String> BUILD_VARS =
135             Arrays.asList(
136                     "build_id",
137                     "build_target",
138                     "branch",
139                     "kernel_build_id",
140                     "kernel_build_target",
141                     "kernel_branch",
142                     "system_build_id",
143                     "system_build_target",
144                     "system_branch",
145                     "emulator_build_id",
146                     "emulator_build_target",
147                     "emulator_branch");
148 
149     private String mInstanceName;
150     private HostAndPort mHostAndPort;
151     private ErrorIdentifier mErrorType;
152     private String mErrors;
153     private GceStatus mStatus;
154     private HashMap<String, String> mBuildVars;
155     private List<LogFileEntry> mLogs;
156     private boolean mIsIpPreconfigured = false;
157     private Integer mDeviceOffset = null;
158     private String mInstanceUser = null;
159     // Skip collecting device log if set to true.
160     private boolean mSkipDeviceLogCollection = false;
161     private boolean mIsOxygenationDevice = false;
162     private String mOxygenationDeviceId = null;
163 
164     public static enum GceStatus {
165         SUCCESS,
166         FAIL,
167         BOOT_FAIL,
168         DEVICE_OFFLINE,
169     }
170 
GceAvdInfo(String instanceName, HostAndPort hostAndPort)171     public GceAvdInfo(String instanceName, HostAndPort hostAndPort) {
172         mInstanceName = instanceName;
173         mHostAndPort = hostAndPort;
174         mBuildVars = new HashMap<String, String>();
175         mLogs = new ArrayList<LogFileEntry>();
176     }
177 
GceAvdInfo( String instanceName, HostAndPort hostAndPort, ErrorIdentifier errorType, String errors, GceStatus status)178     public GceAvdInfo(
179             String instanceName,
180             HostAndPort hostAndPort,
181             ErrorIdentifier errorType,
182             String errors,
183             GceStatus status) {
184         this(instanceName, hostAndPort);
185         mErrorType = errorType;
186         mErrors = errors;
187         mStatus = status;
188     }
189 
190     /** {@inheritDoc} */
191     @Override
toString()192     public String toString() {
193         return "GceAvdInfo [mInstanceName="
194                 + mInstanceName
195                 + ", mHostAndPort="
196                 + mHostAndPort
197                 + ", mDeviceOffset="
198                 + mDeviceOffset
199                 + ", mInstanceUser="
200                 + mInstanceUser
201                 + ", mErrorType="
202                 + mErrorType
203                 + ", mErrors="
204                 + mErrors
205                 + ", mStatus="
206                 + mStatus
207                 + ", mIsIpPreconfigured="
208                 + mIsIpPreconfigured
209                 + ", mBuildVars="
210                 + ", mOxygenationDeviceId="
211                 + mOxygenationDeviceId
212                 + ", mIsOxygenationDevice="
213                 + mIsOxygenationDevice
214                 + mBuildVars.toString()
215                 + ", mLogs="
216                 + mLogs.toString()
217                 + "]";
218     }
219 
isOxygenationDevice()220     public boolean isOxygenationDevice() {
221         return mIsOxygenationDevice;
222     }
223 
getOxygenationDeviceId()224     public String getOxygenationDeviceId() {
225         return mOxygenationDeviceId;
226     }
227 
instanceName()228     public String instanceName() {
229         return mInstanceName;
230     }
231 
hostAndPort()232     public HostAndPort hostAndPort() {
233         return mHostAndPort;
234     }
235 
getErrorType()236     public ErrorIdentifier getErrorType() {
237         return mErrorType;
238     }
239 
setErrorType(ErrorIdentifier errorType)240     public void setErrorType(ErrorIdentifier errorType) {
241         mErrorType = errorType;
242     }
243 
getErrors()244     public String getErrors() {
245         return mErrors;
246     }
247 
setErrors(String errors)248     public void setErrors(String errors) {
249         mErrors = errors;
250     }
251 
252     /** Return the map from local or remote log paths to types. */
getLogs()253     public List<LogFileEntry> getLogs() {
254         return mLogs;
255     }
256 
getStatus()257     public GceStatus getStatus() {
258         return mStatus;
259     }
260 
setStatus(GceStatus status)261     public void setStatus(GceStatus status) {
262         mStatus = status;
263     }
264 
addBuildVar(String buildKey, String buildValue)265     private void addBuildVar(String buildKey, String buildValue) {
266         mBuildVars.put(buildKey, buildValue);
267     }
268 
setIpPreconfigured(boolean isIpPreconfigured)269     public void setIpPreconfigured(boolean isIpPreconfigured) {
270         mIsIpPreconfigured = isIpPreconfigured;
271     }
272 
isIpPreconfigured()273     public boolean isIpPreconfigured() {
274         return mIsIpPreconfigured;
275     }
276 
setDeviceOffset(Integer deviceOffset)277     public void setDeviceOffset(Integer deviceOffset) {
278         mDeviceOffset = deviceOffset;
279     }
280 
getDeviceOffset()281     public Integer getDeviceOffset() {
282         return mDeviceOffset;
283     }
284 
setInstanceUser(String instanceUser)285     public void setInstanceUser(String instanceUser) {
286         mInstanceUser = instanceUser;
287     }
288 
getInstanceUser()289     public String getInstanceUser() {
290         return mInstanceUser;
291     }
292 
293     /**
294      * Return build variable information hash of GCE AVD device.
295      *
296      * <p>Possible build variables keys are described in BUILD_VARS for example: build_id,
297      * build_target, branch, kernel_build_id, kernel_build_target, kernel_branch, system_build_id,
298      * system_build_target, system_branch, emulator_build_id, emulator_build_target,
299      * emulator_branch.
300      */
getBuildVars()301     public HashMap<String, String> getBuildVars() {
302         return new HashMap<String, String>(mBuildVars);
303     }
304 
getSkipDeviceLogCollection()305     public boolean getSkipDeviceLogCollection() {
306         return mSkipDeviceLogCollection;
307     }
308 
309     // TODO(b/329150949): Remove after lab update
setSkipBugreportCollection(boolean skipDeviceLogCollection)310     public void setSkipBugreportCollection(boolean skipDeviceLogCollection) {
311         mSkipDeviceLogCollection = skipDeviceLogCollection;
312     }
313 
setSkipDeviceLogCollection(boolean skipDeviceLogCollection)314     public void setSkipDeviceLogCollection(boolean skipDeviceLogCollection) {
315         mSkipDeviceLogCollection = skipDeviceLogCollection;
316     }
317 
318     /**
319      * Parse a given file to obtain the GCE AVD device info.
320      *
321      * @param f {@link File} file to read the JSON output from GCE Driver.
322      * @param descriptor the descriptor of the device that needs the info.
323      * @param remoteAdbPort the remote port that should be used for adb connection
324      * @return the {@link GceAvdInfo} of the device if found, or null if error.
325      */
parseGceInfoFromFile( File f, DeviceDescriptor descriptor, int remoteAdbPort)326     public static GceAvdInfo parseGceInfoFromFile(
327             File f, DeviceDescriptor descriptor, int remoteAdbPort) throws TargetSetupError {
328         String data;
329         try {
330           data = Files.readString(f.toPath(), StandardCharsets.UTF_8);
331         } catch (IOException e) {
332             CLog.e("Failed to read result file from GCE driver:");
333             CLog.e(e);
334             return null;
335         }
336         return parseGceInfoFromString(data, descriptor, remoteAdbPort);
337     }
338 
339     /**
340      * Parse a given string to obtain the GCE AVD device info.
341      *
342      * @param data JSON string.
343      * @param descriptor the descriptor of the device that needs the info.
344      * @param remoteAdbPort the remote port that should be used for adb connection
345      * @return the {@link GceAvdInfo} of the device if found, or null if error.
346      */
parseGceInfoFromString( String data, DeviceDescriptor descriptor, int remoteAdbPort)347     public static GceAvdInfo parseGceInfoFromString(
348             String data, DeviceDescriptor descriptor, int remoteAdbPort) throws TargetSetupError {
349         if (Strings.isNullOrEmpty(data)) {
350             CLog.w("No data provided");
351             return null;
352         }
353         InfraErrorIdentifier errorId = null;
354         String errors = data;
355         try {
356             errors = parseErrorField(data);
357             JSONObject res = new JSONObject(data);
358             String status = res.getString("status");
359             GceStatus gceStatus = GceStatus.valueOf(status);
360             String errorType = res.has("error_type") ? res.getString("error_type") : null;
361             if (errorType == null) {
362                 // Parse more detailed error type if we can.
363                 if (errors.contains("QUOTA_EXCEED") && errors.contains("GPU")) {
364                     errorType = "ACLOUD_QUOTA_EXCEED_GPU";
365                 }
366             }
367             errorId =
368                     GceStatus.SUCCESS.equals(gceStatus)
369                             ? null
370                             : determineAcloudErrorType(errorType);
371             if (errorId == InfraErrorIdentifier.ACLOUD_OXYGEN_LEASE_ERROR) {
372                 errorId = refineOxygenErrorType(errors);
373             }
374             JSONArray devices = null;
375             if (GceStatus.FAIL.equals(gceStatus) || GceStatus.BOOT_FAIL.equals(gceStatus)) {
376                 // In case of failure we still look for instance name to shutdown if needed.
377                 if (res.getJSONObject("data").has("devices_failing_boot")) {
378                     devices = res.getJSONObject("data").getJSONArray("devices_failing_boot");
379                 }
380             } else {
381                 devices = res.getJSONObject("data").getJSONArray("devices");
382             }
383             if (devices != null) {
384                 if (devices.length() == 1) {
385                     JSONObject d = (JSONObject) devices.get(0);
386                     addCfStartTimeMetrics(d);
387                     String ip = d.getString("ip");
388                     String instanceName = d.getString("instance_name");
389                     GceAvdInfo avdInfo =
390                             new GceAvdInfo(
391                                     instanceName,
392                                     HostAndPort.fromString(ip).withDefaultPort(remoteAdbPort),
393                                     errorId,
394                                     errors,
395                                     gceStatus);
396                     avdInfo.mLogs.addAll(parseLogField(d));
397                     for (String buildVar : BUILD_VARS) {
398                         if (d.has(buildVar) && !d.getString(buildVar).trim().isEmpty()) {
399                             avdInfo.addBuildVar(buildVar, d.getString(buildVar).trim());
400                         }
401                     }
402                     return avdInfo;
403                 } else {
404                     CLog.w("Expected only one device to return but found %d", devices.length());
405                 }
406             } else {
407                 CLog.w("No device information, device was not started.");
408             }
409         } catch (JSONException e) {
410             CLog.e("Failed to parse JSON %s:", data);
411             CLog.e(e);
412         }
413 
414         // If errors are found throw an exception with the acloud message.
415         if (errorId == null) {
416             errorId = InfraErrorIdentifier.ACLOUD_UNDETERMINED;
417         }
418         throw new TargetSetupError(
419                 String.format("acloud errors: %s", !errors.isEmpty() ? errors : data),
420                 descriptor,
421                 errorId);
422     }
423 
424     /**
425      * Parse a given command line output from Oxygen client binary to obtain leased AVD info.
426      *
427      * @param oxygenRes the {@link CommandResult} from Oxygen client command execution.
428      * @param remoteAdbPort the remote port that should be used for adb connection
429      * @return {@link List} of the devices successfully leased. Will throw {@link TargetSetupError}
430      *     if failed to lease a device.
431      */
parseGceInfoFromOxygenClientOutput( CommandResult oxygenRes, int remoteAdbPort)432     public static List<GceAvdInfo> parseGceInfoFromOxygenClientOutput(
433             CommandResult oxygenRes, int remoteAdbPort) throws TargetSetupError {
434         CommandStatus oxygenCliStatus = oxygenRes.getStatus();
435         if (CommandStatus.SUCCESS.equals(oxygenCliStatus)) {
436             return parseSucceedOxygenClientOutput(
437                     oxygenRes.getStdout() + oxygenRes.getStderr(), remoteAdbPort);
438         } else if (CommandStatus.TIMED_OUT.equals(oxygenCliStatus)) {
439             return Arrays.asList(
440                     new GceAvdInfo(
441                             null,
442                             null,
443                             InfraErrorIdentifier.OXYGEN_CLIENT_BINARY_TIMEOUT,
444                             "Oxygen client binary CLI timed out",
445                             GceStatus.FAIL));
446         } else {
447             CLog.d(
448                     "OxygenClient - CommandStatus: %s, output: %s\", output",
449                     oxygenCliStatus, oxygenRes.getStdout() + " " + oxygenRes.getStderr());
450             InfraErrorIdentifier identifier = refineOxygenErrorType(oxygenRes.getStderr());
451             throw new TargetSetupError(
452                     OXYGEN_ERROR_MESSAGE_MAP.getOrDefault(
453                             identifier, "Oxygen client failed to lease a device"),
454                     new Exception(
455                             oxygenRes.getStderr()), // Include the original error message as cause.
456                     identifier);
457         }
458     }
459 
parseSucceedOxygenClientOutput(String output, int remoteAdbPort)460     private static List<GceAvdInfo> parseSucceedOxygenClientOutput(String output, int remoteAdbPort)
461             throws TargetSetupError {
462         CLog.d("Parsing oxygen client output: %s", output);
463 
464         Pattern pattern =
465                 Pattern.compile(
466                         "session_id:\"(.*?)\".*?server_url:\"(.*?)\".*?oxygen_version:\"(.*?)\"",
467                         Pattern.DOTALL);
468         Matcher matcher = pattern.matcher(output);
469 
470         List<GceAvdInfo> gceAvdInfos = new ArrayList<>();
471         int deviceOffset = 0;
472         while (matcher.find()) {
473             String sessionId = matcher.group(1);
474             String serverUrl = matcher.group(2);
475             String oxygenVersion = matcher.group(3);
476             gceAvdInfos.add(
477                     new GceAvdInfo(
478                             sessionId,
479                             HostAndPort.fromString(serverUrl)
480                                     .withDefaultPort(remoteAdbPort + deviceOffset),
481                             null,
482                             null,
483                             GceStatus.SUCCESS));
484             InvocationMetricLogger.addInvocationMetrics(
485                     InvocationMetricKey.CF_OXYGEN_VERSION, oxygenVersion);
486             deviceOffset++;
487         }
488         if (gceAvdInfos.isEmpty()) {
489             throw new TargetSetupError(
490                     String.format("Failed to parse the output: %s", output),
491                     InfraErrorIdentifier.OXYGEN_CLIENT_BINARY_ERROR);
492         }
493 
494         return gceAvdInfos;
495     }
496 
497     /**
498      * Search error message from Oxygen service for more accurate error code.
499      *
500      * @param errors error messages returned by Oxygen service.
501      * @return InfraErrorIdentifier for the Oxygen service error.
502      */
503     @VisibleForTesting
refineOxygenErrorType(String errors)504     static InfraErrorIdentifier refineOxygenErrorType(String errors) {
505         for (Map.Entry<InfraErrorIdentifier, String> entry : OXYGEN_ERROR_PATTERN_MAP.entrySet()) {
506             if (errors.contains(entry.getValue()))
507                 return entry.getKey();
508          }
509 
510         return InfraErrorIdentifier.ACLOUD_OXYGEN_LEASE_ERROR;
511     }
512 
parseErrorField(String data)513     private static String parseErrorField(String data) throws JSONException {
514         String res = "";
515         JSONObject response = new JSONObject(data);
516         JSONArray errors = response.getJSONArray("errors");
517         for (int i = 0; i < errors.length(); i++) {
518             res += (errors.getString(i) + "\n");
519         }
520         return res;
521     }
522 
523     /**
524      * Parse log paths from a device object.
525      *
526      * @param device the device object in JSON.
527      * @return a list of {@link LogFileEntry}.
528      * @throws JSONException if any required property is missing.
529      */
parseLogField(JSONObject device)530     private static List<LogFileEntry> parseLogField(JSONObject device) throws JSONException {
531         List<LogFileEntry> logs = new ArrayList<LogFileEntry>();
532         JSONArray logArray = device.optJSONArray("logs");
533         if (logArray == null) {
534             return logs;
535         }
536         for (int i = 0; i < logArray.length(); i++) {
537             JSONObject logObject = logArray.getJSONObject(i);
538             logs.add(new LogFileEntry(logObject));
539         }
540         return logs;
541     }
542 
543     @VisibleForTesting
determineAcloudErrorType(String errorType)544     static InfraErrorIdentifier determineAcloudErrorType(String errorType) {
545         InfraErrorIdentifier identifier;
546         if (errorType == null || errorType.isEmpty()) {
547             return InfraErrorIdentifier.ACLOUD_UNRECOGNIZED_ERROR_TYPE;
548         }
549         try {
550             identifier = InfraErrorIdentifier.valueOf(errorType);
551         } catch (Exception e) {
552             identifier = InfraErrorIdentifier.ACLOUD_UNRECOGNIZED_ERROR_TYPE;
553         }
554         return identifier;
555     }
556 
557     @VisibleForTesting
addCfStartTimeMetrics(JSONObject json)558     static void addCfStartTimeMetrics(JSONObject json) {
559         // These metrics may not be available for all GCE.
560         String fetch_artifact_time = json.optString("fetch_artifact_time");
561         if (!fetch_artifact_time.isEmpty()) {
562             InvocationMetricLogger.addInvocationMetrics(
563                     InvocationMetricKey.CF_FETCH_ARTIFACT_TIME,
564                     Double.valueOf(Double.parseDouble(fetch_artifact_time) * 1000).longValue());
565         }
566         String gce_create_time = json.optString("gce_create_time");
567         if (!gce_create_time.isEmpty()) {
568             InvocationMetricLogger.addInvocationMetrics(
569                     InvocationMetricKey.CF_GCE_CREATE_TIME,
570                     Double.valueOf(Double.parseDouble(gce_create_time) * 1000).longValue());
571         }
572         String launch_cvd_time = json.optString("launch_cvd_time");
573         if (!launch_cvd_time.isEmpty()) {
574             InvocationMetricLogger.addInvocationMetrics(
575                     InvocationMetricKey.CF_LAUNCH_CVD_TIME,
576                     Double.valueOf(Double.parseDouble(launch_cvd_time) * 1000).longValue());
577         }
578         JSONObject fetch_cvd_wrapper_log = json.optJSONObject("fetch_cvd_wrapper_log");
579         if (fetch_cvd_wrapper_log != null) {
580             String cf_cache_wait_time_sec =
581                     fetch_cvd_wrapper_log.optString("cf_cache_wait_time_sec");
582             if (!Strings.isNullOrEmpty(cf_cache_wait_time_sec)) {
583                 InvocationMetricLogger.addInvocationMetrics(
584                         InvocationMetricKey.CF_CACHE_WAIT_TIME,
585                         Integer.parseInt(cf_cache_wait_time_sec));
586             }
587             String cf_artifacts_fetch_source =
588                     fetch_cvd_wrapper_log.optString("cf_artifacts_fetch_source");
589             if (!Strings.isNullOrEmpty(cf_artifacts_fetch_source)) {
590                 InvocationMetricLogger.addInvocationMetrics(
591                         InvocationMetricKey.CF_ARTIFACTS_FETCH_SOURCE, cf_artifacts_fetch_source);
592             }
593         }
594 
595         if (!InvocationMetricLogger.getInvocationMetrics()
596                 .containsKey(InvocationMetricKey.CF_INSTANCE_COUNT.toString())) {
597             InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.CF_INSTANCE_COUNT, 1);
598         }
599     }
600 }
601