1 /* 2 * Copyright (C) 2019 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.clearcut; 17 18 import com.android.annotations.VisibleForTesting; 19 import com.android.asuite.clearcut.Clientanalytics.ClientInfo; 20 import com.android.asuite.clearcut.Clientanalytics.LogEvent; 21 import com.android.asuite.clearcut.Clientanalytics.LogRequest; 22 import com.android.asuite.clearcut.Clientanalytics.LogResponse; 23 import com.android.asuite.clearcut.Common.UserType; 24 import com.android.tradefed.log.LogUtil.CLog; 25 import com.android.tradefed.util.CommandResult; 26 import com.android.tradefed.util.CommandStatus; 27 import com.android.tradefed.util.FileUtil; 28 import com.android.tradefed.util.RunUtil; 29 import com.android.tradefed.util.StreamUtil; 30 import com.android.tradefed.util.net.HttpHelper; 31 32 import com.google.protobuf.util.JsonFormat; 33 34 import java.io.File; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.io.OutputStream; 38 import java.io.OutputStreamWriter; 39 import java.net.HttpURLConnection; 40 import java.net.InetAddress; 41 import java.net.URL; 42 import java.net.UnknownHostException; 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.UUID; 46 import java.util.concurrent.ScheduledThreadPoolExecutor; 47 import java.util.concurrent.TimeUnit; 48 49 /** Client that allows reporting usage metrics to clearcut. */ 50 public class ClearcutClient { 51 52 public static final String DISABLE_CLEARCUT_KEY = "DISABLE_CLEARCUT"; 53 54 private static final String CLEARCUT_PROD_URL = "https://play.googleapis.com/log"; 55 private static final int CLIENT_TYPE = 1; 56 private static final int INTERNAL_LOG_SOURCE = 971; 57 private static final int EXTERNAL_LOG_SOURCE = 934; 58 59 private static final long SCHEDULER_INITIAL_DELAY_SECONDS = 2; 60 private static final long SCHEDULER_PERDIOC_SECONDS = 30; 61 62 private static final String GOOGLE_EMAIL = "@google.com"; 63 private static final String GOOGLE_HOSTNAME = ".google.com"; 64 65 private File mCachedUuidFile = new File(System.getProperty("user.home"), ".tradefed"); 66 private String mRunId; 67 68 private final int mLogSource; 69 private final String mUrl; 70 private final UserType mUserType; 71 72 // Consider synchronized list 73 private List<LogRequest> mExternalEventQueue; 74 // The pool executor to actually post the metrics 75 private ScheduledThreadPoolExecutor mExecutor; 76 // Whether the clearcut client should be inop 77 private boolean mDisabled = false; 78 ClearcutClient()79 public ClearcutClient() { 80 this(null); 81 } 82 83 /** 84 * Create Client with customized posting URL and forcing whether it's internal or external user. 85 */ 86 @VisibleForTesting ClearcutClient(String url)87 protected ClearcutClient(String url) { 88 mDisabled = isClearcutDisabled(); 89 90 // We still have to set the 'final' variable so go through the assignments before returning 91 if (!mDisabled && isGoogleUser()) { 92 mLogSource = INTERNAL_LOG_SOURCE; 93 mUserType = UserType.GOOGLE; 94 } else { 95 mLogSource = EXTERNAL_LOG_SOURCE; 96 mUserType = UserType.EXTERNAL; 97 } 98 if (url == null) { 99 mUrl = CLEARCUT_PROD_URL; 100 } else { 101 mUrl = url; 102 } 103 mRunId = UUID.randomUUID().toString(); 104 mExternalEventQueue = new ArrayList<>(); 105 106 if (mDisabled) { 107 return; 108 } 109 110 // Print the notice 111 System.out.println(NoticeMessageUtil.getNoticeMessage(mUserType)); 112 113 // Executor to actually send the events. 114 mExecutor = new ScheduledThreadPoolExecutor(1); 115 Runnable command = 116 new Runnable() { 117 @Override 118 public void run() { 119 flushEvents(); 120 } 121 }; 122 mExecutor.scheduleAtFixedRate( 123 command, 124 SCHEDULER_INITIAL_DELAY_SECONDS, 125 SCHEDULER_PERDIOC_SECONDS, 126 TimeUnit.SECONDS); 127 } 128 129 /** Send the first event to notify that Tradefed was started. */ notifyTradefedStartEvent()130 public void notifyTradefedStartEvent() { 131 if (mDisabled) { 132 return; 133 } 134 LogRequest.Builder request = createBaseLogRequest(); 135 LogEvent.Builder logEvent = LogEvent.newBuilder(); 136 logEvent.setEventTimeMs(System.currentTimeMillis()); 137 logEvent.setSourceExtension( 138 ClearcutEventHelper.createStartEvent(getGroupingKey(), mRunId, mUserType)); 139 request.addLogEvent(logEvent); 140 queueEvent(request.build()); 141 } 142 143 /** Stop the periodic sending of clearcut events */ stop()144 public void stop() { 145 if (mExecutor != null) { 146 mExecutor.setRemoveOnCancelPolicy(true); 147 mExecutor.shutdown(); 148 mExecutor = null; 149 } 150 // Send all remaining events 151 flushEvents(); 152 } 153 154 /** Add an event to the queue of events that needs to be send. */ queueEvent(LogRequest event)155 public void queueEvent(LogRequest event) { 156 synchronized (mExternalEventQueue) { 157 mExternalEventQueue.add(event); 158 } 159 } 160 161 /** Returns the current queue size. */ getQueueSize()162 public final int getQueueSize() { 163 synchronized (mExternalEventQueue) { 164 return mExternalEventQueue.size(); 165 } 166 } 167 168 /** Allows to override the default cached uuid file. */ setCachedUuidFile(File uuidFile)169 public void setCachedUuidFile(File uuidFile) { 170 mCachedUuidFile = uuidFile; 171 } 172 173 /** Get a new or the cached uuid for the user. */ 174 @VisibleForTesting getGroupingKey()175 String getGroupingKey() { 176 String uuid = null; 177 if (mCachedUuidFile.exists()) { 178 try { 179 uuid = FileUtil.readStringFromFile(mCachedUuidFile); 180 } catch (IOException e) { 181 CLog.e(e); 182 } 183 } 184 if (uuid == null || uuid.isEmpty()) { 185 uuid = UUID.randomUUID().toString(); 186 try { 187 FileUtil.writeToFile(uuid, mCachedUuidFile); 188 } catch (IOException e) { 189 CLog.e(e); 190 } 191 } 192 return uuid; 193 } 194 195 /** Returns True if clearcut is disabled, False otherwise. */ 196 @VisibleForTesting isClearcutDisabled()197 boolean isClearcutDisabled() { 198 return "1".equals(System.getenv(DISABLE_CLEARCUT_KEY)); 199 } 200 201 /** Returns True if the user is a Googler, False otherwise. */ 202 @VisibleForTesting isGoogleUser()203 boolean isGoogleUser() { 204 CommandResult gitRes = 205 RunUtil.getDefault() 206 .runTimedCmdSilently(60000L, "git", "config", "--get", "user.email"); 207 if (CommandStatus.SUCCESS.equals(gitRes.getStatus())) { 208 String stdout = gitRes.getStdout(); 209 if (stdout != null && stdout.trim().endsWith(GOOGLE_EMAIL)) { 210 return true; 211 } 212 } 213 try { 214 String hostname = InetAddress.getLocalHost().getHostName(); 215 if (hostname.contains(GOOGLE_HOSTNAME)) { 216 return true; 217 } 218 } catch (UnknownHostException e) { 219 // Ignore 220 } 221 return false; 222 } 223 createBaseLogRequest()224 private LogRequest.Builder createBaseLogRequest() { 225 LogRequest.Builder request = LogRequest.newBuilder(); 226 request.setLogSource(mLogSource); 227 request.setClientInfo(ClientInfo.newBuilder().setClientType(CLIENT_TYPE)); 228 return request; 229 } 230 flushEvents()231 private void flushEvents() { 232 List<LogRequest> copy = new ArrayList<>(); 233 synchronized (mExternalEventQueue) { 234 copy.addAll(mExternalEventQueue); 235 mExternalEventQueue.clear(); 236 } 237 while (!copy.isEmpty()) { 238 LogRequest event = copy.remove(0); 239 sendToClearcut(event); 240 } 241 } 242 243 /** Send one event to the configured server. */ sendToClearcut(LogRequest event)244 private void sendToClearcut(LogRequest event) { 245 HttpHelper helper = new HttpHelper(); 246 247 InputStream inputStream = null; 248 InputStream errorStream = null; 249 OutputStream outputStream = null; 250 OutputStreamWriter outputStreamWriter = null; 251 try { 252 HttpURLConnection connection = helper.createConnection(new URL(mUrl), "POST", "text"); 253 outputStream = connection.getOutputStream(); 254 outputStreamWriter = new OutputStreamWriter(outputStream); 255 256 String jsonObject = JsonFormat.printer().preservingProtoFieldNames().print(event); 257 outputStreamWriter.write(jsonObject.toString()); 258 outputStreamWriter.flush(); 259 260 inputStream = connection.getInputStream(); 261 LogResponse response = LogResponse.parseFrom(inputStream); 262 263 errorStream = connection.getErrorStream(); 264 if (errorStream != null) { 265 String message = StreamUtil.getStringFromStream(errorStream); 266 CLog.e("Error posting clearcut event: '%s'. LogResponse: '%s'", message, response); 267 } 268 } catch (IOException e) { 269 CLog.e(e); 270 } finally { 271 StreamUtil.close(outputStream); 272 StreamUtil.close(inputStream); 273 StreamUtil.close(outputStreamWriter); 274 StreamUtil.close(errorStream); 275 } 276 } 277 } 278