blob: 0a6368967400fb0cdad9d92472406e49c9a3a581 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// 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_recorder.h"
#include <sstream>
#include <utility>
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/metrics_hashes.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/task/current_thread.h"
#include "components/metrics/metrics_features.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/project_validator.h"
#include "components/metrics/structured/storage.pb.h"
#include "components/metrics/structured/structured_metrics_features.h"
#include "components/metrics/structured/structured_metrics_validator.h"
#include "third_party/metrics_proto/chrome_user_metrics_extension.pb.h"
namespace metrics::structured {
namespace {
using ::metrics::ChromeUserMetricsExtension;
using ::metrics::SystemProfileProto;
// 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;
// Directory containing serialized event protos to read.
constexpr char kExternalMetricsDir[] = "/var/lib/metrics/structured/events";
} // namespace
int StructuredMetricsRecorder::kMaxEventsPerUpload = 100;
char StructuredMetricsRecorder::kProfileKeyDataPath[] =
"structured_metrics/keys";
char StructuredMetricsRecorder::kDeviceKeyDataPath[] =
"/var/lib/metrics/structured/chromium/keys";
char StructuredMetricsRecorder::kUnsentLogsPath[] = "structured_metrics/events";
StructuredMetricsRecorder::StructuredMetricsRecorder(
base::raw_ptr<metrics::MetricsProvider> system_profile_provider)
: StructuredMetricsRecorder(base::FilePath(kDeviceKeyDataPath),
base::Milliseconds(kSaveDelayMs),
system_profile_provider) {}
StructuredMetricsRecorder::StructuredMetricsRecorder(
const base::FilePath& device_key_path,
base::TimeDelta write_delay,
base::raw_ptr<metrics::MetricsProvider> system_profile_provider)
: device_key_path_(device_key_path),
write_delay_(write_delay),
system_profile_provider_(system_profile_provider) {
DCHECK(system_profile_provider_);
Recorder::GetInstance()->AddObserver(this);
}
StructuredMetricsRecorder::~StructuredMetricsRecorder() {
Recorder::GetInstance()->RemoveObserver(this);
DCHECK(!IsInObserverList());
}
void StructuredMetricsRecorder::EnableRecording() {
DCHECK(base::CurrentUIThread::IsSet());
// Enable recording only if structured metrics' feature flag is enabled.
recording_enabled_ =
base::FeatureList::IsEnabled(features::kStructuredMetrics);
if (external_metrics_.get() != nullptr) {
external_metrics_->EnableRecording();
}
if (recording_enabled_) {
CacheDisallowedProjectsSet();
}
}
void StructuredMetricsRecorder::DisableRecording() {
DCHECK(base::CurrentUIThread::IsSet());
recording_enabled_ = false;
if (external_metrics_.get() != nullptr) {
external_metrics_->DisableRecording();
}
disallowed_projects_.clear();
}
void StructuredMetricsRecorder::ProvideUmaEventMetrics(
ChromeUserMetricsExtension& uma_proto) {
if (!can_provide_metrics()) {
return;
}
auto* structured_data = uma_proto.mutable_structured_data();
structured_data->mutable_events()->Swap(events()->mutable_uma_events());
events()->clear_uma_events();
proto().StartWrite();
LogUploadSizeBytes(structured_data->ByteSizeLong());
}
void StructuredMetricsRecorder::ProvideEventMetrics(
ChromeUserMetricsExtension& uma_proto) {
if (!can_provide_metrics()) {
return;
}
LogNumEventsInUpload(events_.get()->get()->non_uma_events_size());
ProvideSystemProfile(uma_proto.mutable_system_profile());
auto* structured_data = uma_proto.mutable_structured_data();
structured_data->mutable_events()->Swap(events()->mutable_non_uma_events());
events()->clear_non_uma_events();
proto().StartWrite();
LogUploadSizeBytes(structured_data->ByteSizeLong());
LogExternalMetricsScanInUpload(external_metrics_scans_);
external_metrics_scans_ = 0;
// Applies custom metadata providers.
Recorder::GetInstance()->OnProvideIndependentMetrics(&uma_proto);
}
void StructuredMetricsRecorder::OnKeyDataInitialized() {
DCHECK(base::CurrentUIThread::IsSet());
++init_count_;
if (init_count_ == kTargetInitCount) {
init_state_ = InitState::kInitialized;
HashUnhashedEventsAndPersist();
}
}
void StructuredMetricsRecorder::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;
HashUnhashedEventsAndPersist();
}
}
void StructuredMetricsRecorder::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 StructuredMetricsRecorder::OnExternalMetricsCollected(
const EventsProto& events) {
DCHECK(base::CurrentUIThread::IsSet());
if (recording_enabled_) {
events_.get()->get()->mutable_uma_events()->MergeFrom(events.uma_events());
events_.get()->get()->mutable_non_uma_events()->MergeFrom(
events.non_uma_events());
// Only increment if new events were add.
if (events.uma_events_size() || events.non_uma_events_size()) {
external_metrics_scans_ += 1;
}
}
}
void StructuredMetricsRecorder::Purge() {
// Only purge if the recorder has been initialized.
if (!is_init_state(InitState::kInitialized)) {
return;
}
DCHECK(events_ && profile_key_data_ && device_key_data_);
events_->Purge();
profile_key_data_->Purge();
device_key_data_->Purge();
}
void StructuredMetricsRecorder::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;
profile_key_data_ = std::make_unique<KeyData>(
profile_path.Append(kProfileKeyDataPath), write_delay_,
base::BindOnce(&StructuredMetricsRecorder::OnKeyDataInitialized,
weak_factory_.GetWeakPtr()));
device_key_data_ = std::make_unique<KeyData>(
base::FilePath(device_key_path_), write_delay_,
base::BindOnce(&StructuredMetricsRecorder::OnKeyDataInitialized,
weak_factory_.GetWeakPtr()));
events_ = std::make_unique<PersistentProto<EventsProto>>(
profile_path.Append(kUnsentLogsPath), write_delay_,
base::BindOnce(&StructuredMetricsRecorder::OnRead,
weak_factory_.GetWeakPtr()),
base::BindRepeating(&StructuredMetricsRecorder::OnWrite,
weak_factory_.GetWeakPtr()));
external_metrics_ = std::make_unique<ExternalMetrics>(
base::FilePath(kExternalMetricsDir),
base::Minutes(kExternalMetricsIntervalMins),
base::BindRepeating(
&StructuredMetricsRecorder::OnExternalMetricsCollected,
weak_factory_.GetWeakPtr()));
if (recording_enabled_) {
external_metrics_->EnableRecording();
}
// See DisableRecording for more information.
if (purge_state_on_init_) {
Purge();
purge_state_on_init_ = false;
}
}
void StructuredMetricsRecorder::OnEventRecord(const Event& event) {
DCHECK(base::CurrentUIThread::IsSet());
// One more state for the EventRecordingState exists: kMetricsProviderMissing.
// This is recorded in Recorder::Record.
if (!recording_enabled_) {
// Events should be ignored if recording is disabled.
LogEventRecordingState(EventRecordingState::kRecordingDisabled);
return;
} else if (init_state_ != InitState::kInitialized) {
// If keys have not loaded yet, then hold the data in memory until the
// keys have been loaded.
LogEventRecordingState(EventRecordingState::kProviderUninitialized);
RecordEventBeforeInitialization(event);
return;
}
DCHECK(profile_key_data_->is_initialized());
DCHECK(device_key_data_->is_initialized());
RecordEvent(event);
events_->QueueWrite();
}
absl::optional<int> StructuredMetricsRecorder::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 StructuredMetricsRecorder::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 StructuredMetricsRecorder::OnSystemProfileInitialized() {
system_profile_initialized_ = true;
}
void StructuredMetricsRecorder::ProvideSystemProfile(
SystemProfileProto* system_profile) {
// Populate the proto if the system profile has been initialized and
// have a system profile provider.
// The field may be populated if ChromeOSMetricsProvider has already run.
if (system_profile_initialized_) {
system_profile_provider_->ProvideSystemProfileMetrics(system_profile);
}
}
void StructuredMetricsRecorder::WriteNowForTest() {
events_->StartWrite();
}
void StructuredMetricsRecorder::SetExternalMetricsDirForTest(
const base::FilePath& dir) {
external_metrics_ = std::make_unique<ExternalMetrics>(
dir, base::Minutes(kExternalMetricsIntervalMins),
base::BindRepeating(
&StructuredMetricsRecorder::OnExternalMetricsCollected,
weak_factory_.GetWeakPtr()));
}
void StructuredMetricsRecorder::RecordEventBeforeInitialization(
const Event& event) {
DCHECK_NE(init_state_, InitState::kInitialized);
unhashed_events_.emplace_back(event.Clone());
}
void StructuredMetricsRecorder::RecordEvent(const Event& event) {
// Validates the event. If valid, retrieve the metadata associated
// with the event.
auto maybe_project_validator =
validator::GetProjectValidator(event.project_name());
DCHECK(maybe_project_validator.has_value());
if (!maybe_project_validator.has_value()) {
return;
}
const auto* project_validator = maybe_project_validator.value();
const auto maybe_event_validator =
project_validator->GetEventValidator(event.event_name());
DCHECK(maybe_event_validator.has_value());
if (!maybe_event_validator.has_value()) {
return;
}
const auto* event_validator = maybe_event_validator.value();
if (!CanUploadProject(project_validator->project_hash())) {
LogEventRecordingState(EventRecordingState::kProjectDisallowed);
return;
}
LogEventRecordingState(EventRecordingState::kRecorded);
// 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 (project_validator->id_type() == IdType::kUmaId ||
!IsIndependentMetricsUploadEnabled()) {
event_proto = events_.get()->get()->add_uma_events();
} else {
event_proto = events_.get()->get()->add_non_uma_events();
}
event_proto->set_project_name_hash(project_validator->project_hash());
// Sequence-related metadata.
if (project_validator->event_type() ==
StructuredEventProto_EventType_SEQUENCE &&
base::FeatureList::IsEnabled(kEventSequenceLogging)) {
auto* event_sequence_metadata =
event_proto->mutable_event_sequence_metadata();
event_sequence_metadata->set_reset_counter(
event.event_sequence_metadata().reset_counter);
event_sequence_metadata->set_system_uptime(
event.recorded_time_since_boot().InMilliseconds());
event_sequence_metadata->set_event_unique_id(
base::HashMetricName(event.event_sequence_metadata().event_unique_id));
event_proto->set_device_project_id(
device_key_data_.get()->Id(project_validator->project_hash(),
project_validator->key_rotation_period()));
event_proto->set_user_project_id(
profile_key_data_.get()->Id(project_validator->project_hash(),
project_validator->key_rotation_period()));
}
// Choose which KeyData to use for this event.
KeyData* key_data;
switch (project_validator->id_scope()) {
case IdScope::kPerProfile:
key_data = profile_key_data_.get();
break;
case IdScope::kPerDevice:
// For event sequence, use the profile key for now to hash strings.
//
// TODO(crbug/1399632): Event sequence is considered a structured
// metrics project. Once the client supports device/profile split of
// events like structured metrics, remove this.
if (project_validator->event_type() ==
StructuredEventProto_EventType_SEQUENCE) {
key_data = profile_key_data_.get();
} else {
key_data = device_key_data_.get();
}
break;
default:
// In case id_scope is uninitialized.
NOTREACHED();
}
// Set the ID for this event, if any.
switch (project_validator->id_type()) {
case IdType::kProjectId:
event_proto->set_profile_event_id(
key_data->Id(project_validator->project_hash(),
project_validator->key_rotation_period()));
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 (project_validator->event_type()) {
case StructuredEventProto_EventType_REGULAR:
case StructuredEventProto_EventType_RAW_STRING:
case StructuredEventProto_EventType_SEQUENCE:
event_proto->set_event_type(project_validator->event_type());
break;
default:
NOTREACHED();
break;
}
event_proto->set_event_name_hash(event_validator->event_hash());
// Set each metric's name hash and value.
for (const auto& metric : event.metric_values()) {
const std::string& metric_name = metric.first;
const Event::MetricValue& metric_value = metric.second;
// Validate that both name and metric type are valid structured metrics.
// If a metric is invalid, then ignore the metric so that other valid
// metrics are added to the proto.
absl::optional<EventValidator::MetricMetadata> metadata =
event_validator->GetMetricMetadata(metric_name);
// Checks that the metrics defined are valid. If not valid, then the
// metric will be ignored.
bool is_valid =
metadata.has_value() && metadata->metric_type == metric_value.type;
DCHECK(is_valid);
if (!is_valid) {
continue;
}
StructuredEventProto::Metric* metric_proto = event_proto->add_metrics();
int64_t metric_name_hash = metadata->metric_name_hash;
metric_proto->set_name_hash(metric_name_hash);
const auto& value = metric_value.value;
switch (metadata->metric_type) {
case Event::MetricType::kHmac:
metric_proto->set_value_hmac(key_data->HmacMetric(
project_validator->project_hash(), metric_name_hash,
value.GetString(), project_validator->key_rotation_period()));
break;
case Event::MetricType::kLong:
int64_t long_value;
base::StringToInt64(value.GetString(), &long_value);
metric_proto->set_value_int64(long_value);
break;
case Event::MetricType::kRawString:
metric_proto->set_value_string(value.GetString());
break;
case Event::MetricType::kDouble:
metric_proto->set_value_double(value.GetDouble());
break;
// Not supported yet.
case Event::MetricType::kInt:
case Event::MetricType::kBoolean:
break;
}
}
// Log size information about the event.
LogEventSerializedSizeBytes(event_proto->ByteSizeLong());
}
void StructuredMetricsRecorder::HashUnhashedEventsAndPersist() {
LogNumEventsRecordedBeforeInit(unhashed_events_.size());
while (!unhashed_events_.empty()) {
RecordEvent(unhashed_events_.front());
unhashed_events_.pop_front();
}
}
bool StructuredMetricsRecorder::CanUploadProject(
uint64_t project_name_hash) const {
return !disallowed_projects_.contains(project_name_hash);
}
void StructuredMetricsRecorder::CacheDisallowedProjectsSet() {
const std::string& disallowed_list = GetDisabledProjects();
if (disallowed_list.empty()) {
return;
}
for (const auto& value :
base::SplitString(disallowed_list, ",", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
uint64_t project_name_hash;
// Parse the string and keep only perfect conversions.
if (base::StringToUint64(value, &project_name_hash)) {
disallowed_projects_.insert(project_name_hash);
}
}
}
void StructuredMetricsRecorder::AddDisallowedProjectForTest(
uint64_t project_name_hash) {
disallowed_projects_.insert(project_name_hash);
}
} // namespace metrics::structured