blob: 50c4244793b1f8c7d4d89116dda946769f662be3 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.clearcut;
import com.android.annotations.VisibleForTesting;
import com.android.asuite.clearcut.Clientanalytics.ClientInfo;
import com.android.asuite.clearcut.Clientanalytics.LogEvent;
import com.android.asuite.clearcut.Clientanalytics.LogRequest;
import com.android.asuite.clearcut.Clientanalytics.LogResponse;
import com.android.asuite.clearcut.Common.UserType;
import com.android.tradefed.invoker.tracing.CloseableTraceScope;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.net.HttpHelper;
import com.google.common.base.Strings;
import com.google.protobuf.util.JsonFormat;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
/** Client that allows reporting usage metrics to clearcut. */
public class ClearcutClient {
public static final String DISABLE_CLEARCUT_KEY = "DISABLE_CLEARCUT";
private static final String CLEARCUT_SUB_TOOL_NAME = "CLEARCUT_SUB_TOOL_NAME";
private static final String CLEARCUT_PROD_URL = "https://play.googleapis.com/log";
private static final int CLIENT_TYPE = 1;
private static final int INTERNAL_LOG_SOURCE = 971;
private static final int EXTERNAL_LOG_SOURCE = 934;
private static final long SCHEDULER_INITIAL_DELAY_MILLISECONDS = 1000;
private static final long SCHEDULER_PERDIOC_MILLISECONDS = 250;
private static final String GOOGLE_EMAIL = "@google.com";
private static final String GOOGLE_HOSTNAME = ".google.com";
private File mCachedUuidFile = new File(System.getProperty("user.home"), ".tradefed");
private String mRunId;
private long mSessionStartTime = 0L;
private final int mLogSource;
private final String mUrl;
private final UserType mUserType;
private final String mSubToolName;
// Consider synchronized list
private List<LogRequest> mExternalEventQueue;
// The pool executor to actually post the metrics
private ScheduledThreadPoolExecutor mExecutor;
// Whether the clearcut client should be inop
private boolean mDisabled = false;
public ClearcutClient(String subToolName) {
this(null, subToolName);
}
/**
* Create Client with customized posting URL and forcing whether it's internal or external user.
*/
@VisibleForTesting
protected ClearcutClient(String url, String subToolName) {
mDisabled = isClearcutDisabled();
// We still have to set the 'final' variable so go through the assignments before returning
if (!mDisabled && isGoogleUser()) {
mLogSource = INTERNAL_LOG_SOURCE;
mUserType = UserType.GOOGLE;
} else {
mLogSource = EXTERNAL_LOG_SOURCE;
mUserType = UserType.EXTERNAL;
}
if (url == null) {
mUrl = CLEARCUT_PROD_URL;
} else {
mUrl = url;
}
mRunId = UUID.randomUUID().toString();
mExternalEventQueue = new ArrayList<>();
if (Strings.isNullOrEmpty(subToolName) && System.getenv(CLEARCUT_SUB_TOOL_NAME) != null) {
mSubToolName = System.getenv(CLEARCUT_SUB_TOOL_NAME);
} else {
mSubToolName = subToolName;
}
if (mDisabled) {
return;
}
// Print the notice
System.out.println(NoticeMessageUtil.getNoticeMessage(mUserType));
// Executor to actually send the events.
mExecutor =
new ScheduledThreadPoolExecutor(
1,
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
t.setName("clearcut-client-thread");
return t;
}
});
Runnable command =
new Runnable() {
@Override
public void run() {
flushEvents();
}
};
mExecutor.scheduleAtFixedRate(
command,
SCHEDULER_INITIAL_DELAY_MILLISECONDS,
SCHEDULER_PERDIOC_MILLISECONDS,
TimeUnit.MILLISECONDS);
}
/** Send the first event to notify that Tradefed was started. */
public void notifyTradefedStartEvent() {
if (mDisabled) {
return;
}
mSessionStartTime = System.nanoTime();
long eventTimeMs = System.currentTimeMillis();
CompletableFuture.supplyAsync(() -> createStartEvent(eventTimeMs));
}
private boolean createStartEvent(long eventTimeMs) {
LogRequest.Builder request = createBaseLogRequest();
LogEvent.Builder logEvent = LogEvent.newBuilder();
logEvent.setEventTimeMs(eventTimeMs);
logEvent.setSourceExtension(
ClearcutEventHelper.createStartEvent(
getGroupingKey(), mRunId, mUserType, mSubToolName));
request.addLogEvent(logEvent);
queueEvent(request.build());
return true;
}
/** Send the last event to notify that Tradefed is done. */
public void notifyTradefedFinishedEvent() {
if (mDisabled) {
return;
}
Duration duration = java.time.Duration.ofNanos(System.nanoTime() - mSessionStartTime);
LogRequest.Builder request = createBaseLogRequest();
LogEvent.Builder logEvent = LogEvent.newBuilder();
logEvent.setEventTimeMs(System.currentTimeMillis());
logEvent.setSourceExtension(
ClearcutEventHelper.createFinishedEvent(
getGroupingKey(), mRunId, mUserType, mSubToolName, duration));
request.addLogEvent(logEvent);
queueEvent(request.build());
}
/** Send the event to notify that a Tradefed invocation was started. */
public void notifyTradefedInvocationStartEvent() {
if (mDisabled) {
return;
}
LogRequest.Builder request = createBaseLogRequest();
LogEvent.Builder logEvent = LogEvent.newBuilder();
logEvent.setEventTimeMs(System.currentTimeMillis());
logEvent.setSourceExtension(
ClearcutEventHelper.createRunStartEvent(
getGroupingKey(), mRunId, mUserType, mSubToolName));
request.addLogEvent(logEvent);
queueEvent(request.build());
}
/** Send the event to notify that a test run finished. */
public void notifyTestRunFinished(long startTimeNano) {
if (mDisabled) {
return;
}
Duration duration = java.time.Duration.ofNanos(System.nanoTime() - startTimeNano);
LogRequest.Builder request = createBaseLogRequest();
LogEvent.Builder logEvent = LogEvent.newBuilder();
logEvent.setEventTimeMs(System.currentTimeMillis());
logEvent.setSourceExtension(
ClearcutEventHelper.creatRunTestFinished(
getGroupingKey(), mRunId, mUserType, mSubToolName, duration));
request.addLogEvent(logEvent);
queueEvent(request.build());
}
/** Stop the periodic sending of clearcut events */
public void stop() {
if (mExecutor != null) {
mExecutor.setRemoveOnCancelPolicy(true);
mExecutor.shutdown();
mExecutor = null;
}
// Send all remaining events
flushEvents();
}
/** Add an event to the queue of events that needs to be send. */
public void queueEvent(LogRequest event) {
synchronized (mExternalEventQueue) {
mExternalEventQueue.add(event);
}
}
/** Returns the current queue size. */
public final int getQueueSize() {
synchronized (mExternalEventQueue) {
return mExternalEventQueue.size();
}
}
/** Allows to override the default cached uuid file. */
public void setCachedUuidFile(File uuidFile) {
mCachedUuidFile = uuidFile;
}
/** Get a new or the cached uuid for the user. */
@VisibleForTesting
String getGroupingKey() {
String uuid = null;
if (mCachedUuidFile.exists()) {
try {
uuid = FileUtil.readStringFromFile(mCachedUuidFile);
} catch (IOException e) {
CLog.e(e);
}
}
if (uuid == null || uuid.isEmpty()) {
uuid = UUID.randomUUID().toString();
try {
FileUtil.writeToFile(uuid, mCachedUuidFile);
} catch (IOException e) {
CLog.e(e);
}
}
return uuid;
}
/** Returns True if clearcut is disabled, False otherwise. */
@VisibleForTesting
public boolean isClearcutDisabled() {
return "1".equals(System.getenv(DISABLE_CLEARCUT_KEY));
}
/** Returns True if the user is a Googler, False otherwise. */
@VisibleForTesting
boolean isGoogleUser() {
try {
String hostname = InetAddress.getLocalHost().getHostName();
if (hostname.contains(GOOGLE_HOSTNAME)) {
return true;
}
} catch (UnknownHostException e) {
// Ignore
}
CommandResult gitRes =
RunUtil.getDefault()
.runTimedCmdSilently(60000L, "git", "config", "--get", "user.email");
if (CommandStatus.SUCCESS.equals(gitRes.getStatus())) {
String stdout = gitRes.getStdout();
if (stdout != null && stdout.trim().endsWith(GOOGLE_EMAIL)) {
return true;
}
}
return false;
}
private LogRequest.Builder createBaseLogRequest() {
LogRequest.Builder request = LogRequest.newBuilder();
request.setLogSource(mLogSource);
request.setClientInfo(ClientInfo.newBuilder().setClientType(CLIENT_TYPE));
return request;
}
private void flushEvents() {
List<LogRequest> copy = new ArrayList<>();
synchronized (mExternalEventQueue) {
copy.addAll(mExternalEventQueue);
mExternalEventQueue.clear();
}
List<CompletableFuture<Boolean>> futures = new ArrayList<>();
while (!copy.isEmpty()) {
LogRequest event = copy.remove(0);
futures.add(CompletableFuture.supplyAsync(() -> sendToClearcut(event)));
}
for (CompletableFuture<Boolean> future : futures) {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
CLog.e(e);
}
}
}
/** Send one event to the configured server. */
private boolean sendToClearcut(LogRequest event) {
HttpHelper helper = new HttpHelper();
InputStream inputStream = null;
InputStream errorStream = null;
OutputStream outputStream = null;
OutputStreamWriter outputStreamWriter = null;
try (CloseableTraceScope ignored = new CloseableTraceScope("sendToClearcut")) {
HttpURLConnection connection = helper.createConnection(new URL(mUrl), "POST", "text");
outputStream = connection.getOutputStream();
outputStreamWriter = new OutputStreamWriter(outputStream);
String jsonObject = JsonFormat.printer().preservingProtoFieldNames().print(event);
outputStreamWriter.write(jsonObject.toString());
outputStreamWriter.flush();
inputStream = connection.getInputStream();
LogResponse response = LogResponse.parseFrom(inputStream);
errorStream = connection.getErrorStream();
if (errorStream != null) {
String message = StreamUtil.getStringFromStream(errorStream);
CLog.e("Error posting clearcut event: '%s'. LogResponse: '%s'", message, response);
}
} catch (IOException e) {
CLog.e(e);
} finally {
StreamUtil.close(outputStream);
StreamUtil.close(inputStream);
StreamUtil.close(outputStreamWriter);
StreamUtil.close(errorStream);
}
return true;
}
}