• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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