// Copyright (c) 2012 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 "chrome/browser/metrics/metrics_log_serializer.h" #include "base/base64.h" #include "base/md5.h" #include "base/metrics/histogram.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/browser/prefs/scoped_user_pref_update.h" #include "chrome/common/pref_names.h" namespace { // The number of "initial" logs to save, and hope to send during a future Chrome // session. Initial logs contain crash stats, and are pretty small. const size_t kInitialLogsPersistLimit = 20; // The number of ongoing logs to save persistently, and hope to // send during a this or future sessions. Note that each log may be pretty // large, as presumably the related "initial" log wasn't sent (probably nothing // was, as the user was probably off-line). As a result, the log probably kept // accumulating while the "initial" log was stalled, and couldn't be sent. As a // result, we don't want to save too many of these mega-logs. // A "standard shutdown" will create a small log, including just the data that // was not yet been transmitted, and that is normal (to have exactly one // ongoing_log_ at startup). const size_t kOngoingLogsPersistLimit = 8; // The number of bytes each of initial and ongoing logs that must be stored. // This ensures that a reasonable amount of history will be stored even if there // is a long series of very small logs. const size_t kStorageByteLimitPerLogType = 300000; // We append (2) more elements to persisted lists: the size of the list and a // checksum of the elements. const size_t kChecksumEntryCount = 2; // TODO(isherman): Remove this histogram once it's confirmed that there are no // encoding failures for protobuf logs. enum LogStoreStatus { STORE_SUCCESS, // Successfully presisted log. ENCODE_FAIL, // Failed to encode log. COMPRESS_FAIL, // Failed to compress log. END_STORE_STATUS // Number of bins to use to create the histogram. }; MetricsLogSerializer::LogReadStatus MakeRecallStatusHistogram( MetricsLogSerializer::LogReadStatus status, bool is_xml) { if (is_xml) { UMA_HISTOGRAM_ENUMERATION("PrefService.PersistentLogRecall", status, MetricsLogSerializer::END_RECALL_STATUS); } else { UMA_HISTOGRAM_ENUMERATION("PrefService.PersistentLogRecallProtobufs", status, MetricsLogSerializer::END_RECALL_STATUS); } return status; } void MakeStoreStatusHistogram(LogStoreStatus status) { UMA_HISTOGRAM_ENUMERATION("PrefService.PersistentLogStore2", status, END_STORE_STATUS); } } // namespace MetricsLogSerializer::MetricsLogSerializer() {} MetricsLogSerializer::~MetricsLogSerializer() {} void MetricsLogSerializer::SerializeLogs( const std::vector& logs, MetricsLogManager::LogType log_type) { PrefService* local_state = g_browser_process->local_state(); DCHECK(local_state); const char* pref_xml = NULL; const char* pref_proto = NULL; size_t store_length_limit = 0; switch (log_type) { case MetricsLogManager::INITIAL_LOG: pref_xml = prefs::kMetricsInitialLogsXml; pref_proto = prefs::kMetricsInitialLogsProto; store_length_limit = kInitialLogsPersistLimit; break; case MetricsLogManager::ONGOING_LOG: pref_xml = prefs::kMetricsOngoingLogsXml; pref_proto = prefs::kMetricsOngoingLogsProto; store_length_limit = kOngoingLogsPersistLimit; break; default: NOTREACHED(); return; }; // Write the XML version. ListPrefUpdate update_xml(local_state, pref_xml); WriteLogsToPrefList(logs, true, store_length_limit, kStorageByteLimitPerLogType, update_xml.Get()); // Write the protobuf version. ListPrefUpdate update_proto(local_state, pref_proto); WriteLogsToPrefList(logs, false, store_length_limit, kStorageByteLimitPerLogType, update_proto.Get()); } void MetricsLogSerializer::DeserializeLogs( MetricsLogManager::LogType log_type, std::vector* logs) { DCHECK(logs); PrefService* local_state = g_browser_process->local_state(); DCHECK(local_state); const char* pref_xml; const char* pref_proto; if (log_type == MetricsLogManager::INITIAL_LOG) { pref_xml = prefs::kMetricsInitialLogsXml; pref_proto = prefs::kMetricsInitialLogsProto; } else { pref_xml = prefs::kMetricsOngoingLogsXml; pref_proto = prefs::kMetricsOngoingLogsProto; } const ListValue* unsent_logs_xml = local_state->GetList(pref_xml); const ListValue* unsent_logs_proto = local_state->GetList(pref_proto); if (ReadLogsFromPrefList(*unsent_logs_xml, true, logs) == RECALL_SUCCESS) { // In order to try to keep the data sent to both servers roughly in sync, // only read the protobuf data if we read the XML data successfully. ReadLogsFromPrefList(*unsent_logs_proto, false, logs); } } // static void MetricsLogSerializer::WriteLogsToPrefList( const std::vector& local_list, bool is_xml, size_t list_length_limit, size_t byte_limit, base::ListValue* list) { // One of the limit arguments must be non-zero. DCHECK(list_length_limit > 0 || byte_limit > 0); list->Clear(); if (local_list.size() == 0) return; size_t start = 0; // If there are too many logs, keep the most recent logs up to the length // limit, and at least to the minimum number of bytes. if (local_list.size() > list_length_limit) { start = local_list.size(); size_t bytes_used = 0; for (std::vector::const_reverse_iterator it = local_list.rbegin(); it != local_list.rend(); ++it) { size_t log_size = it->length(); if (bytes_used >= byte_limit && (local_list.size() - start) >= list_length_limit) break; bytes_used += log_size; --start; } } DCHECK_LT(start, local_list.size()); if (start >= local_list.size()) return; // Store size at the beginning of the list. list->Append(Value::CreateIntegerValue(local_list.size() - start)); base::MD5Context ctx; base::MD5Init(&ctx); std::string encoded_log; for (std::vector::const_iterator it = local_list.begin() + start; it != local_list.end(); ++it) { const std::string& value = is_xml ? it->xml : it->proto; // We encode the compressed log as Value::CreateStringValue() expects to // take a valid UTF8 string. if (!base::Base64Encode(value, &encoded_log)) { MakeStoreStatusHistogram(ENCODE_FAIL); list->Clear(); return; } base::MD5Update(&ctx, encoded_log); list->Append(Value::CreateStringValue(encoded_log)); } // Append hash to the end of the list. base::MD5Digest digest; base::MD5Final(&digest, &ctx); list->Append(Value::CreateStringValue(base::MD5DigestToBase16(digest))); DCHECK(list->GetSize() >= 3); // Minimum of 3 elements (size, data, hash). MakeStoreStatusHistogram(STORE_SUCCESS); } // static MetricsLogSerializer::LogReadStatus MetricsLogSerializer::ReadLogsFromPrefList( const ListValue& list, bool is_xml, std::vector* local_list) { if (list.GetSize() == 0) return MakeRecallStatusHistogram(LIST_EMPTY, is_xml); if (list.GetSize() < 3) return MakeRecallStatusHistogram(LIST_SIZE_TOO_SMALL, is_xml); // The size is stored at the beginning of the list. int size; bool valid = (*list.begin())->GetAsInteger(&size); if (!valid) return MakeRecallStatusHistogram(LIST_SIZE_MISSING, is_xml); // Account for checksum and size included in the list. if (static_cast(size) != list.GetSize() - kChecksumEntryCount) { return MakeRecallStatusHistogram(LIST_SIZE_CORRUPTION, is_xml); } // Allocate strings for all of the logs we are going to read in. // Do this ahead of time so that we can decode the string values directly into // the elements of |local_list|, and thereby avoid making copies of the // serialized logs, which can be fairly large. if (is_xml) { DCHECK(local_list->empty()); local_list->resize(size); } else if (local_list->size() != static_cast(size)) { return MakeRecallStatusHistogram(XML_PROTO_MISMATCH, false); } base::MD5Context ctx; base::MD5Init(&ctx); std::string encoded_log; size_t local_index = 0; for (ListValue::const_iterator it = list.begin() + 1; it != list.end() - 1; // Last element is the checksum. ++it, ++local_index) { bool valid = (*it)->GetAsString(&encoded_log); if (!valid) { local_list->clear(); return MakeRecallStatusHistogram(LOG_STRING_CORRUPTION, is_xml); } base::MD5Update(&ctx, encoded_log); DCHECK_LT(local_index, local_list->size()); std::string& decoded_log = is_xml ? (*local_list)[local_index].xml : (*local_list)[local_index].proto; if (!base::Base64Decode(encoded_log, &decoded_log)) { local_list->clear(); return MakeRecallStatusHistogram(DECODE_FAIL, is_xml); } } // Verify checksum. base::MD5Digest digest; base::MD5Final(&digest, &ctx); std::string recovered_md5; // We store the hash at the end of the list. valid = (*(list.end() - 1))->GetAsString(&recovered_md5); if (!valid) { local_list->clear(); return MakeRecallStatusHistogram(CHECKSUM_STRING_CORRUPTION, is_xml); } if (recovered_md5 != base::MD5DigestToBase16(digest)) { local_list->clear(); return MakeRecallStatusHistogram(CHECKSUM_CORRUPTION, is_xml); } return MakeRecallStatusHistogram(RECALL_SUCCESS, is_xml); }