blob: 01478e02335d26a9e6757f720706d426a1a53f0b [file] [log] [blame]
// Copyright 2016 The Android Open Source Project
//
// This software is licensed under the terms of the GNU General Public
// License version 2, as published by the Free Software Foundation, and
// may be copied, distributed, and modified under those terms.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "android/metrics/MetricsReporter.h"
#include <assert.h> // for assert
#include <stdio.h> // for FILE, stdout
#include <type_traits> // for swap, integral...
#include <utility> // for move
#include "android/base/Optional.h" // for Optional
#include "android/base/async/ThreadLooper.h" // for ThreadLooper
#include "android/base/files/PathUtils.h" // for pj
#include "android/base/files/StdioStream.h" // for StdioStream
#include "android/base/memory/LazyInstance.h" // for LazyInstance
#include "android/base/threads/Async.h" // for async
#include "android/cmdline-option.h" // for AndroidOptions
#include "android/metrics/AsyncMetricsReporter.h" // for AsyncMetricsRe...
#include "android/metrics/CrashMetricsReporting.h" // for reportCrashMet...
#include "android/metrics/FileMetricsWriter.h" // for FileMetricsWriter
#include "android/metrics/MetricsPaths.h" // for getSpoolDirectory
#include "android/metrics/NullMetricsReporter.h" // for NullMetricsRep...
#include "android/metrics/PlaystoreMetricsWriter.h" // for PlaystoreMetri...
#include "android/metrics/StudioConfig.h" // for getAnonymizati...
#include "android/metrics/TextMetricsWriter.h" // for TextMetricsWriter
#include "android/metrics/proto/google_logs_publishing.pb.h" // for LogEvent
#include "android/metrics/proto/studio_stats.pb.h" // for AndroidStudioE...
#include "android/utils/debug.h" // for dwarning
#include "android/utils/file_io.h" // for android_fopen
#include "picosha2.h" // for hash256_one_by...
using android::base::System;
namespace android {
namespace metrics {
namespace {
static base::LazyInstance<NullMetricsReporter> sNullInstance = {};
// A small class that ensures there's always an instance of metrics reporter
// ready to process a metrics write request.
// By default it's an instance of NullMetricsReporter() which discards all
// requests, but it can be reset to anything that implements MetricsReporter
// interface.
class ReporterHolder final {
public:
ReporterHolder() : mPtr(sNullInstance.ptr()) {}
void reset(MetricsReporter::Ptr newPtr) {
if (newPtr) {
mPtr = newPtr.release();
} else {
// Replace the current instance with a null one and delete the old
// reporter (if it wasn't already a null reporter.
MetricsReporter* other = sNullInstance.ptr();
std::swap(mPtr, other);
if (other != sNullInstance.ptr()) {
delete other;
}
}
}
MetricsReporter& reporter() const { return *mPtr; }
private:
MetricsReporter* mPtr;
};
static base::LazyInstance<ReporterHolder> sInstance = {};
} // namespace
void MetricsReporter::start(const std::string& sessionId,
base::StringView emulatorVersion,
base::StringView emulatorFullVersion,
base::StringView qemuVersion) {
MetricsWriter::Ptr writer;
if (android_cmdLineOptions->metrics_to_console) {
writer = TextMetricsWriter::create(base::StdioStream(stdout));
} else if (android_cmdLineOptions->metrics_collection) {
writer = PlaystoreMetricsWriter::create(sessionId,
base::pj(getSpoolDirectory(), "backoff_cookie.proto"));
} else if (android_cmdLineOptions->metrics_to_file != nullptr) {
if (FILE* out = ::android_fopen(android_cmdLineOptions->metrics_to_file,
"w")) {
writer = TextMetricsWriter::create(
base::StdioStream(out, base::StdioStream::kOwner));
} else {
dwarning("Failed to open file '%s', disabling metrics reporting",
android_cmdLineOptions->metrics_to_file);
}
} else if (studio::getUserMetricsOptIn()) {
writer = FileMetricsWriter::create(
getSpoolDirectory(), sessionId,
1000, // record limit per single file
base::ThreadLooper::get(),
10 * 60 * 1000); // time limit for a single file, ms
}
if (!writer) {
sInstance->reset({});
} else {
sInstance->reset(Ptr(new AsyncMetricsReporter(
writer, emulatorVersion, emulatorFullVersion, qemuVersion)));
// Run the asynchronous cleanup/reporting job now.
base::async([] {
const auto sessions =
FileMetricsWriter::finalizeAbandonedSessionFiles(
getSpoolDirectory());
reportCrashMetrics(get(), sessions);
});
}
}
void MetricsReporter::stop(MetricsStopReason reason) {
for (const auto& callback : sInstance->reporter().mOnExit) {
sInstance->reporter().report(callback);
}
sInstance->reporter().report(
[reason](android_studio::AndroidStudioEvent* event) {
int crashCount = reason != METRICS_STOP_GRACEFUL ? 1 : 0;
event->mutable_emulator_details()->set_crashes(crashCount);
});
sInstance->reset({});
}
void MetricsReporter::reportOnExit(Callback callback) {
if (callback) {
mOnExit.push_back(callback);
}
}
MetricsReporter& MetricsReporter::get() {
return sInstance->reporter();
}
void MetricsReporter::report(Callback callback) {
if (!callback) {
return;
}
reportConditional([callback](android_studio::AndroidStudioEvent* event) {
callback(event);
return true;
});
}
MetricsReporter::MetricsReporter(bool enabled,
MetricsWriter::Ptr writer,
base::StringView emulatorVersion,
base::StringView emulatorFullVersion,
base::StringView qemuVersion)
: mWriter(std::move(writer)),
mEnabled(enabled),
mStartTimeMs(System::get()->getUnixTimeUs() / 1000),
mEmulatorVersion(emulatorVersion),
mEmulatorFullVersion(emulatorFullVersion),
mQemuVersion(qemuVersion) {
assert(mWriter);
}
MetricsReporter::~MetricsReporter() = default;
bool MetricsReporter::isReportingEnabled() const {
return mEnabled;
}
const std::string& MetricsReporter::sessionId() const {
// Protect this from unexpected changes in the MetricsWriter interface.
static_assert(std::is_reference<decltype(mWriter->sessionId())>::value,
"MetricsWriter::sessionId() must return a reference");
return mWriter->sessionId();
}
std::string MetricsReporter::anonymize(base::StringView s) {
picosha2::hash256_one_by_one hasher;
hasher.process(s.begin(), s.end());
const auto salt = this->salt();
hasher.process(salt.begin(), salt.end());
hasher.finish();
return picosha2::get_hash_hex_string(hasher);
}
const base::System::Duration MetricsReporter::getStartTimeMs() {
return mStartTimeMs;
}
void MetricsReporter::sendToWriter(android_studio::AndroidStudioEvent* event) {
wireless_android_play_playlog::LogEvent logEvent;
const auto timeMs = System::get()->getUnixTimeUs() / 1000;
logEvent.set_event_time_ms(timeMs);
if (!event->has_kind()) {
event->set_kind(android_studio::AndroidStudioEvent::EMULATOR_PING);
}
event->mutable_product_details()->set_product(
android_studio::ProductDetails::EMULATOR);
if (!mEmulatorVersion.empty()) {
event->mutable_product_details()->set_version(mEmulatorVersion);
}
if (!mEmulatorFullVersion.empty()) {
event->mutable_product_details()->set_build(mEmulatorFullVersion);
}
if (!mQemuVersion.empty()) {
event->mutable_emulator_details()->set_core_version(mQemuVersion);
}
const auto times = System::get()->getProcessTimes();
event->mutable_emulator_details()->set_system_time(times.systemMs);
event->mutable_emulator_details()->set_user_time(times.userMs);
event->mutable_emulator_details()->set_wall_time(times.wallClockMs);
// Only set the session ID if it isn't set: some messages might be reported
// on behalf of a different (e.g. crashed) session.
if (!event->has_studio_session_id()) {
event->set_studio_session_id(sessionId());
}
mWriter->write(*event, &logEvent);
}
std::string MetricsReporter::salt() {
const auto modTime =
base::System::get()->pathModificationTime(getSettingsFilePath());
base::AutoLock lock(mSaltLock);
if (mSalt.empty() || modTime.valueOr(0) != mSaltFileTime) {
auto salt = studio::getAnonymizationSalt();
mSaltFileTime = modTime.valueOr(0);
mSalt = std::move(salt);
}
return mSalt;
}
} // namespace metrics
} // namespace android