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 /** Send the event to notify that a Tradefed invocation was started. */ notifyTradefedInvocationStartEvent()144 public void notifyTradefedInvocationStartEvent() { 145 if (mDisabled) { 146 return; 147 } 148 LogRequest.Builder request = createBaseLogRequest(); 149 LogEvent.Builder logEvent = LogEvent.newBuilder(); 150 logEvent.setEventTimeMs(System.currentTimeMillis()); 151 logEvent.setSourceExtension( 152 ClearcutEventHelper.createRunStartEvent(getGroupingKey(), mRunId, mUserType)); 153 request.addLogEvent(logEvent); 154 queueEvent(request.build()); 155 } 156 157 /** Stop the periodic sending of clearcut events */ stop()158 public void stop() { 159 if (mExecutor != null) { 160 mExecutor.setRemoveOnCancelPolicy(true); 161 mExecutor.shutdown(); 162 mExecutor = null; 163 } 164 // Send all remaining events 165 flushEvents(); 166 } 167 168 /** Add an event to the queue of events that needs to be send. */ queueEvent(LogRequest event)169 public void queueEvent(LogRequest event) { 170 synchronized (mExternalEventQueue) { 171 mExternalEventQueue.add(event); 172 } 173 } 174 175 /** Returns the current queue size. */ getQueueSize()176 public final int getQueueSize() { 177 synchronized (mExternalEventQueue) { 178 return mExternalEventQueue.size(); 179 } 180 } 181 182 /** Allows to override the default cached uuid file. */ setCachedUuidFile(File uuidFile)183 public void setCachedUuidFile(File uuidFile) { 184 mCachedUuidFile = uuidFile; 185 } 186 187 /** Get a new or the cached uuid for the user. */ 188 @VisibleForTesting getGroupingKey()189 String getGroupingKey() { 190 String uuid = null; 191 if (mCachedUuidFile.exists()) { 192 try { 193 uuid = FileUtil.readStringFromFile(mCachedUuidFile); 194 } catch (IOException e) { 195 CLog.e(e); 196 } 197 } 198 if (uuid == null || uuid.isEmpty()) { 199 uuid = UUID.randomUUID().toString(); 200 try { 201 FileUtil.writeToFile(uuid, mCachedUuidFile); 202 } catch (IOException e) { 203 CLog.e(e); 204 } 205 } 206 return uuid; 207 } 208 209 /** Returns True if clearcut is disabled, False otherwise. */ 210 @VisibleForTesting isClearcutDisabled()211 boolean isClearcutDisabled() { 212 return "1".equals(System.getenv(DISABLE_CLEARCUT_KEY)); 213 } 214 215 /** Returns True if the user is a Googler, False otherwise. */ 216 @VisibleForTesting isGoogleUser()217 boolean isGoogleUser() { 218 CommandResult gitRes = 219 RunUtil.getDefault() 220 .runTimedCmdSilently(60000L, "git", "config", "--get", "user.email"); 221 if (CommandStatus.SUCCESS.equals(gitRes.getStatus())) { 222 String stdout = gitRes.getStdout(); 223 if (stdout != null && stdout.trim().endsWith(GOOGLE_EMAIL)) { 224 return true; 225 } 226 } 227 try { 228 String hostname = InetAddress.getLocalHost().getHostName(); 229 if (hostname.contains(GOOGLE_HOSTNAME)) { 230 return true; 231 } 232 } catch (UnknownHostException e) { 233 // Ignore 234 } 235 return false; 236 } 237 createBaseLogRequest()238 private LogRequest.Builder createBaseLogRequest() { 239 LogRequest.Builder request = LogRequest.newBuilder(); 240 request.setLogSource(mLogSource); 241 request.setClientInfo(ClientInfo.newBuilder().setClientType(CLIENT_TYPE)); 242 return request; 243 } 244 flushEvents()245 private void flushEvents() { 246 List<LogRequest> copy = new ArrayList<>(); 247 synchronized (mExternalEventQueue) { 248 copy.addAll(mExternalEventQueue); 249 mExternalEventQueue.clear(); 250 } 251 while (!copy.isEmpty()) { 252 LogRequest event = copy.remove(0); 253 sendToClearcut(event); 254 } 255 } 256 257 /** Send one event to the configured server. */ sendToClearcut(LogRequest event)258 private void sendToClearcut(LogRequest event) { 259 HttpHelper helper = new HttpHelper(); 260 261 InputStream inputStream = null; 262 InputStream errorStream = null; 263 OutputStream outputStream = null; 264 OutputStreamWriter outputStreamWriter = null; 265 try { 266 HttpURLConnection connection = helper.createConnection(new URL(mUrl), "POST", "text"); 267 outputStream = connection.getOutputStream(); 268 outputStreamWriter = new OutputStreamWriter(outputStream); 269 270 String jsonObject = JsonFormat.printer().preservingProtoFieldNames().print(event); 271 outputStreamWriter.write(jsonObject.toString()); 272 outputStreamWriter.flush(); 273 274 inputStream = connection.getInputStream(); 275 LogResponse response = LogResponse.parseFrom(inputStream); 276 277 errorStream = connection.getErrorStream(); 278 if (errorStream != null) { 279 String message = StreamUtil.getStringFromStream(errorStream); 280 CLog.e("Error posting clearcut event: '%s'. LogResponse: '%s'", message, response); 281 } 282 } catch (IOException e) { 283 CLog.e(e); 284 } finally { 285 StreamUtil.close(outputStream); 286 StreamUtil.close(inputStream); 287 StreamUtil.close(outputStreamWriter); 288 StreamUtil.close(errorStream); 289 } 290 } 291 } 292