blob: 3114014df7a876e2ad4f8f5b8818400fd8b8dc22 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/metrics/structured/structured_metrics_provider.h"
#include <utility>
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/current_thread.h"
#include "components/metrics/structured/enums.h"
#include "components/metrics/structured/external_metrics.h"
#include "components/metrics/structured/histogram_util.h"
#include "components/metrics/structured/storage.pb.h"
#include "components/metrics/structured/structured_metrics_features.h"
#include "third_party/metrics_proto/chrome_user_metrics_extension.pb.h"
namespace metrics {
namespace structured {
namespace {
using ::metrics::ChromeUserMetricsExtension;
// The delay period for the PersistentProto.
constexpr int kSaveDelayMs = 1000;
// The interval between chrome's collection of metrics logged from cros.
constexpr int kExternalMetricsIntervalMins = 10;
// The minimum waiting time between successive deliveries of independent metrics
// to the metrics service via ProvideIndependentMetrics. This is set carefully:
// metrics logs are stored in a queue of limited size, and are uploaded roughly
// every 30 minutes.
constexpr base::TimeDelta kMinIndependentMetricsInterval = base::Minutes(45);
// Directory containing serialized event protos to read.
constexpr char kExternalMetricsDir[] = "/var/lib/metrics/structured/events";
} // namespace
int StructuredMetricsProvider::kMaxEventsPerUpload = 100;
char StructuredMetricsProvider::kProfileKeyDataPath[] =
"structured_metrics/keys";
char StructuredMetricsProvider::kDeviceKeyDataPath[] =
"/var/lib/metrics/structured/chromium/keys";
char StructuredMetricsProvider::kUnsentLogsPath[] = "structured_metrics/events";
StructuredMetricsProvider::StructuredMetricsProvider() {
Recorder::GetInstance()->AddObserver(this);
}
StructuredMetricsProvider::~StructuredMetricsProvider() {
Recorder::GetInstance()->RemoveObserver(this);
DCHECK(!IsInObserverList());
}
void StructuredMetricsProvider::OnKeyDataInitialized() {
DCHECK(base::CurrentUIThread::IsSet());
++init_count_;
if (init_count_ == kTargetInitCount) {
init_state_ = InitState::kInitialized;
}
}
void StructuredMetricsProvider::OnRead(const ReadStatus status) {
DCHECK(base::CurrentUIThread::IsSet());
switch (status) {
case ReadStatus::kOk:
case ReadStatus::kMissing:
break;
case ReadStatus::kReadError:
LogInternalError(StructuredMetricsError::kEventReadError);
break;
case ReadStatus::kParseError:
LogInternalError(StructuredMetricsError::kEventParseError);
break;
}
++init_count_;
if (init_count_ == kTargetInitCount) {
init_state_ = InitState::kInitialized;
}
}
void StructuredMetricsProvider::OnWrite(const WriteStatus status) {
DCHECK(base::CurrentUIThread::IsSet());
switch (status) {
case WriteStatus::kOk:
break;
case WriteStatus::kWriteError:
LogInternalError(StructuredMetricsError::kEventWriteError);
break;
case WriteStatus::kSerializationError:
LogInternalError(StructuredMetricsError::kEventSerializationError);
break;
}
}
void StructuredMetricsProvider::OnExternalMetricsCollected(
const EventsProto& events) {
DCHECK(base::CurrentUIThread::IsSet());
events_.get()->get()->mutable_uma_events()->MergeFrom(events.uma_events());
events_.get()->get()->mutable_non_uma_events()->MergeFrom(
events.non_uma_events());
}
void StructuredMetricsProvider::Purge() {
DCHECK(events_ && profile_key_data_ && device_key_data_);
events_->Purge();
profile_key_data_->Purge();
device_key_data_->Purge();
}
void StructuredMetricsProvider::OnProfileAdded(
const base::FilePath& profile_path) {
DCHECK(base::CurrentUIThread::IsSet());
// We do not handle multiprofile, instead initializing with the state stored
// in the first logged-in user's cryptohome. So if a second profile is added
// we should ignore it. All init state beyond |InitState::kUninitialized| mean
// a profile has already been added.
if (init_state_ != InitState::kUninitialized)
return;
init_state_ = InitState::kProfileAdded;
const auto save_delay = base::Milliseconds(kSaveDelayMs);
profile_key_data_ = std::make_unique<KeyData>(
profile_path.Append(kProfileKeyDataPath), save_delay,
base::BindOnce(&StructuredMetricsProvider::OnKeyDataInitialized,
weak_factory_.GetWeakPtr()));
// TODO(crbug.com/1148168): Change this to receive the key data path in the
// constructor and avoid the test-specific logic.
const auto device_key_data_path = device_key_data_path_for_test_.has_value()
? device_key_data_path_for_test_.value()
: base::FilePath(kDeviceKeyDataPath);
device_key_data_ = std::make_unique<KeyData>(
base::FilePath(device_key_data_path), save_delay,
base::BindOnce(&StructuredMetricsProvider::OnKeyDataInitialized,
weak_factory_.GetWeakPtr()));
events_ = std::make_unique<PersistentProto<EventsProto>>(
profile_path.Append(kUnsentLogsPath), save_delay,
base::BindOnce(&StructuredMetricsProvider::OnRead,
weak_factory_.GetWeakPtr()),
base::BindRepeating(&StructuredMetricsProvider::OnWrite,
weak_factory_.GetWeakPtr()));
external_metrics_ = std::make_unique<ExternalMetrics>(
base::FilePath(kExternalMetricsDir),
base::Minutes(kExternalMetricsIntervalMins),
base::BindRepeating(
&StructuredMetricsProvider::OnExternalMetricsCollected,
weak_factory_.GetWeakPtr()));
// See OnRecordingDisabled for more information.
if (purge_state_on_init_) {
Purge();
purge_state_on_init_ = false;
}
}
void StructuredMetricsProvider::OnRecord(const EventBase& event) {
DCHECK(base::CurrentUIThread::IsSet());
// One more state for the EventRecordingState exists: kMetricsProviderMissing.
// This is recorded in Recorder::Record.
if (!recording_enabled_) {
LogEventRecordingState(EventRecordingState::kRecordingDisabled);
} else if (init_state_ != InitState::kInitialized) {
LogEventRecordingState(EventRecordingState::kProviderUninitialized);
} else {
LogEventRecordingState(EventRecordingState::kRecorded);
}
if (!recording_enabled_ || init_state_ != InitState::kInitialized)
return;
DCHECK(profile_key_data_->is_initialized());
DCHECK(device_key_data_->is_initialized());
// TODO(crbug.com/1148168): We are transitioning to new upload behaviour for
// non-client_id-identified metrics. See structured_metrics_features.h for
// more information. If IsIndependentMetricsUploadEnabled below returns false,
// this reverts to the old behaviour. The call can be removed once we are
// confident with the change.
// The |events_| persistent proto contains two repeated fields, uma_events
// and non_uma_events. uma_events is added to the ChromeUserMetricsExtension
// on a call to ProvideCurrentSessionData, which is the standard UMA upload
// and contains the UMA client_id. non_uma_events is added to the proto on
// a call to ProvideIndependentMetrics, which is a separate upload that does
// _not_ contain the UMA client_id.
//
// We decide which field to add this event to based on the event's IdType.
// kUmaId events should go in the UMA upload, and all others in the non-UMA
// upload.
StructuredEventProto* event_proto;
if (event.id_type() == IdType::kUmaId ||
!IsIndependentMetricsUploadEnabled()) {
event_proto = events_.get()->get()->add_uma_events();
} else {
event_proto = events_.get()->get()->add_non_uma_events();
}
// Choose which KeyData to use for this event.
KeyData* key_data;
switch (event.id_scope()) {
case IdScope::kPerProfile:
key_data = profile_key_data_.get();
break;
case IdScope::kPerDevice:
key_data = device_key_data_.get();
break;
default:
// In case id_scope is uninitialized.
NOTREACHED();
}
// Set the ID for this event, if any.
switch (event.id_type()) {
case IdType::kProjectId:
event_proto->set_profile_event_id(
key_data->Id(event.project_name_hash()));
break;
case IdType::kUmaId:
// TODO(crbug.com/1148168): Unimplemented.
break;
case IdType::kUnidentified:
// Do nothing.
break;
default:
// In case id_type is uninitialized.
NOTREACHED();
break;
}
// Set the event type. Do this with a switch statement to catch when the event
// type is UNKNOWN or uninitialized.
switch (event.event_type()) {
case StructuredEventProto_EventType_REGULAR:
case StructuredEventProto_EventType_RAW_STRING:
event_proto->set_event_type(event.event_type());
break;
default:
NOTREACHED();
break;
}
event_proto->set_event_name_hash(event.name_hash());
// Set each metric's name hash and value.
for (const auto& metric : event.metrics()) {
auto* metric_proto = event_proto->add_metrics();
metric_proto->set_name_hash(metric.name_hash);
switch (metric.type) {
case EventBase::MetricType::kHmac:
metric_proto->set_value_hmac(key_data->HmacMetric(
event.project_name_hash(), metric.name_hash, metric.hmac_value));
break;
case EventBase::MetricType::kInt:
metric_proto->set_value_int64(metric.int_value);
break;
case EventBase::MetricType::kRawString:
metric_proto->set_value_string(metric.string_value);
break;
}
}
events_->QueueWrite();
}
absl::optional<int> StructuredMetricsProvider::LastKeyRotation(
const uint64_t project_name_hash) {
DCHECK(base::CurrentUIThread::IsSet());
if (init_state_ != InitState::kInitialized)
return absl::nullopt;
DCHECK(profile_key_data_->is_initialized());
DCHECK(device_key_data_->is_initialized());
// |project_name_hash| could store its keys in either the profile or device
// key data, so check both. As they cannot both contain the same name hash, at
// most one will return a non-nullopt value.
absl::optional<int> profile_day =
profile_key_data_->LastKeyRotation(project_name_hash);
absl::optional<int> device_day =
device_key_data_->LastKeyRotation(project_name_hash);
DCHECK(!(profile_day && device_day));
return profile_day ? profile_day : device_day;
}
void StructuredMetricsProvider::OnRecordingEnabled() {
DCHECK(base::CurrentUIThread::IsSet());
// Enable recording only if structured metrics' feature flag is enabled.
recording_enabled_ = base::FeatureList::IsEnabled(kStructuredMetrics);
}
void StructuredMetricsProvider::OnRecordingDisabled() {
DCHECK(base::CurrentUIThread::IsSet());
recording_enabled_ = false;
}
void StructuredMetricsProvider::OnReportingStateChanged(bool enabled) {
DCHECK(base::CurrentUIThread::IsSet());
// When reporting is enabled, OnRecordingEnabled is also called. Let that
// handle enabling.
if (enabled) {
return;
}
// When reporting is disabled, OnRecordingDisabled is also called. Disabling
// here is redundant but done for clarity.
recording_enabled_ = false;
// Delete keys and unsent logs. We need to handle two cases:
//
// 1. A profile hasn't been added yet and we can't delete the files
// immediately. In this case set |purge_state_on_init_| and let
// OnProfileAdded call Purge after initialization.
//
// 2. A profile has been added and so the backing PersistentProtos have been
// constructed. In this case just call Purge directly.
//
// Note that Purge will ensure the events are deleted from disk even if the
// PersistentProto hasn't itself finished being read.
if (init_state_ == InitState::kUninitialized) {
purge_state_on_init_ = true;
} else {
Purge();
}
}
void StructuredMetricsProvider::ProvideCurrentSessionData(
ChromeUserMetricsExtension* uma_proto) {
DCHECK(base::CurrentUIThread::IsSet());
if (!recording_enabled_ || init_state_ != InitState::kInitialized) {
return;
}
LogNumEventsInUpload(events_.get()->get()->uma_events_size());
auto* structured_data = uma_proto->mutable_structured_data();
structured_data->mutable_events()->Swap(
events_.get()->get()->mutable_uma_events());
events_.get()->get()->clear_uma_events();
events_->StartWrite();
}
bool StructuredMetricsProvider::HasIndependentMetrics() {
if (!IsIndependentMetricsUploadEnabled()) {
return false;
}
if (!recording_enabled_ || init_state_ != InitState::kInitialized) {
return false;
}
if (base::Time::Now() - last_provided_independent_metrics_ <
kMinIndependentMetricsInterval) {
return false;
}
return events_.get()->get()->non_uma_events_size() != 0;
}
void StructuredMetricsProvider::ProvideIndependentMetrics(
base::OnceCallback<void(bool)> done_callback,
ChromeUserMetricsExtension* uma_proto,
base::HistogramSnapshotManager*) {
DCHECK(base::CurrentUIThread::IsSet());
if (!recording_enabled_ || init_state_ != InitState::kInitialized) {
std::move(done_callback).Run(false);
return;
}
last_provided_independent_metrics_ = base::Time::Now();
LogNumEventsInUpload(events_.get()->get()->non_uma_events_size());
auto* structured_data = uma_proto->mutable_structured_data();
structured_data->mutable_events()->Swap(
events_.get()->get()->mutable_non_uma_events());
events_.get()->get()->clear_non_uma_events();
events_->StartWrite();
// Independent events should not be associated with the client_id, so clear
// it.
uma_proto->clear_client_id();
std::move(done_callback).Run(true);
}
void StructuredMetricsProvider::WriteNowForTest() {
events_->StartWrite();
}
void StructuredMetricsProvider::SetExternalMetricsDirForTest(
const base::FilePath& dir) {
external_metrics_ = std::make_unique<ExternalMetrics>(
dir, base::Minutes(kExternalMetricsIntervalMins),
base::BindRepeating(
&StructuredMetricsProvider::OnExternalMetricsCollected,
weak_factory_.GetWeakPtr()));
}
void StructuredMetricsProvider::SetDeviceKeyDataPathForTest(
const base::FilePath& path) {
// Updating the path after a profile has been added will have no effect, so
// make it an error.
DCHECK_EQ(init_state_, InitState::kUninitialized);
device_key_data_path_for_test_ = path;
}
} // namespace structured
} // namespace metrics