blob: 61b5ae8e3aa6226984985d2ac9c8f931ba10f48c [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 "content/browser/tracing/trace_report_database.h"
#include <optional>
#include <string>
#include <vector>
#include "base/files/file_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/sequence_checker.h"
#include "base/token.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
namespace content {
namespace {
const base::FilePath::CharType kLocalTracesDatabasePath[] =
FILE_PATH_LITERAL("LocalTraces.db");
const char kLocalTracesTableName[] = "local_traces";
constexpr int kCurrentVersionNumber = 5;
ClientTraceReport GetReportFromStatement(sql::Statement& statement) {
auto trace_id = base::Token::FromString(statement.ColumnStringView(0));
CHECK(trace_id.has_value());
ClientTraceReport client_report;
client_report.uuid = *trace_id;
client_report.creation_time = statement.ColumnTime(1);
client_report.scenario_name = statement.ColumnString(2);
client_report.upload_rule_name = statement.ColumnString(3);
if (statement.GetColumnType(4) != sql::ColumnType::kNull) {
client_report.upload_rule_value = statement.ColumnInt(4);
}
client_report.upload_state =
static_cast<ReportUploadState>(statement.ColumnInt(5));
client_report.upload_time = statement.ColumnTime(6);
client_report.skip_reason =
static_cast<SkipUploadReason>(statement.ColumnInt(7));
client_report.has_trace_content = statement.ColumnBool(8);
client_report.total_size = static_cast<uint64_t>(statement.ColumnInt64(9));
return client_report;
}
// create table `local_traces` with following columns:
// `uuid` is the unique ID of the trace.
// `creation_time` The date and time in seconds when the row was created.
// `scenario_name` The trace scenario name.
// `upload_rule_name` The name of the rule that triggered the upload.
// `upload_rule_value` The value of the rule that triggered the upload.
// `state` The current upload state of the trace.
// `upload_time` Time at which the trace was uploaded. NULL if not uploaded.
// `skip_reason` Reason why a trace was not uploaded.
// `trace_content` The serialized trace content string
// `system_profile` The serialized system profile string
// `file_size` The size of trace in bytes.
constexpr char kLocalTracesTableSql[] =
// clang-format off
"CREATE TABLE IF NOT EXISTS local_traces("
"uuid TEXT PRIMARY KEY NOT NULL,"
"creation_time DATETIME NOT NULL,"
"scenario_name TEXT NOT NULL,"
"upload_rule_name TEXT NOT NULL,"
"upload_rule_value INT NULL,"
"state INT NOT NULL,"
"upload_time DATETIME NULL,"
"skip_reason INT NOT NULL,"
"trace_content BLOB NULL,"
"system_profile BLOB NULL,"
"file_size INTEGER NOT NULL)";
// clang-format on
} // namespace
BaseTraceReport::BaseTraceReport() = default;
BaseTraceReport::BaseTraceReport(const BaseTraceReport& other) = default;
BaseTraceReport::~BaseTraceReport() = default;
NewTraceReport::NewTraceReport() = default;
NewTraceReport::~NewTraceReport() = default;
ClientTraceReport::ClientTraceReport() = default;
ClientTraceReport::~ClientTraceReport() = default;
TraceReportDatabase::TraceReportDatabase()
: database_(sql::DatabaseOptions().set_cache_size(128),
/*tag=*/"LocalTraces") {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
bool TraceReportDatabase::OpenDatabase(const base::FilePath& path) {
if (database_.is_open()) {
DCHECK_EQ(db_file_path_, path.Append(kLocalTracesDatabasePath));
return EnsureTableCreated();
}
db_file_path_ = path.Append(kLocalTracesDatabasePath);
const base::FilePath dir = db_file_path_.DirName();
if (!base::DirectoryExists(dir) && !base::CreateDirectory(dir)) {
return false;
}
if (!database_.Open(db_file_path_)) {
return false;
}
return EnsureTableCreated();
}
bool TraceReportDatabase::OpenDatabaseInMemoryForTesting() {
if (database_.is_open()) {
return EnsureTableCreated();
}
if (!database_.OpenInMemory()) {
return false;
}
return EnsureTableCreated();
}
bool TraceReportDatabase::OpenDatabaseIfExists(const base::FilePath& path) {
if (database_.is_open()) {
DCHECK_EQ(db_file_path_, path.Append(kLocalTracesDatabasePath));
return database_.DoesTableExist(kLocalTracesTableName);
}
db_file_path_ = path.Append(kLocalTracesDatabasePath);
const base::FilePath dir = db_file_path_.DirName();
if (!base::DirectoryExists(dir)) {
return false;
}
if (!database_.Open(db_file_path_)) {
return false;
}
if (!database_.DoesTableExist(kLocalTracesTableName)) {
return false;
}
return EnsureTableCreated();
}
bool TraceReportDatabase::AddTrace(const NewTraceReport& new_report) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement create_local_trace(
database_.GetCachedStatement(SQL_FROM_HERE, R"sql(
INSERT INTO local_traces(
uuid, creation_time, scenario_name, upload_rule_name,
upload_rule_value, state, upload_time, skip_reason, trace_content,
file_size, system_profile) VALUES(?,?,?,?,?,?,?,?,?,?,?))sql"));
CHECK(create_local_trace.is_valid());
create_local_trace.BindString(0, new_report.uuid.ToString());
create_local_trace.BindTime(1, new_report.creation_time);
create_local_trace.BindString(2, new_report.scenario_name);
create_local_trace.BindString(3, new_report.upload_rule_name);
if (new_report.upload_rule_value) {
create_local_trace.BindInt(4, *new_report.upload_rule_value);
} else {
create_local_trace.BindNull(4);
}
create_local_trace.BindInt(
5, new_report.skip_reason == SkipUploadReason::kNoSkip
? static_cast<int>(ReportUploadState::kPending)
: static_cast<int>(ReportUploadState::kNotUploaded));
create_local_trace.BindNull(6);
create_local_trace.BindInt(7, static_cast<int>(new_report.skip_reason));
if (!new_report.trace_content.empty()) {
create_local_trace.BindBlob(8, new_report.trace_content);
} else {
create_local_trace.BindNull(8);
}
create_local_trace.BindInt64(9, new_report.total_size);
create_local_trace.BindBlob(10, new_report.system_profile);
return create_local_trace.Run();
}
bool TraceReportDatabase::UserRequestedUpload(const base::Token& uuid) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement update_local_trace(
database_.GetCachedStatement(SQL_FROM_HERE,
"UPDATE local_traces "
"SET state=? "
"WHERE uuid=?"
"AND NOT skip_reason=?"));
CHECK(update_local_trace.is_valid());
update_local_trace.BindInt(
0, static_cast<int>(ReportUploadState::kPending_UserRequested));
update_local_trace.BindString(1, uuid.ToString());
update_local_trace.BindInt(
2, static_cast<int>(SkipUploadReason::kNotAnonymized));
return update_local_trace.Run();
}
bool TraceReportDatabase::UploadComplete(const base::Token& uuid,
base::Time time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement update_local_trace(
database_.GetCachedStatement(SQL_FROM_HERE,
R"sql(UPDATE local_traces
SET state=?, upload_time=?,
system_profile=NULL
WHERE uuid=?)sql"));
CHECK(update_local_trace.is_valid());
update_local_trace.BindInt(0, static_cast<int>(ReportUploadState::kUploaded));
update_local_trace.BindTime(1, time);
update_local_trace.BindString(2, uuid.ToString());
return update_local_trace.Run();
}
bool TraceReportDatabase::UploadSkipped(const base::Token& uuid,
SkipUploadReason skip_reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement update_local_trace(
database_.GetCachedStatement(SQL_FROM_HERE,
R"sql(UPDATE local_traces
SET state=?, skip_reason=?
WHERE uuid=?)sql"));
CHECK(update_local_trace.is_valid());
update_local_trace.BindInt(0,
static_cast<int>(ReportUploadState::kNotUploaded));
update_local_trace.BindInt(1, static_cast<int>(skip_reason));
update_local_trace.BindString(2, uuid.ToString());
return update_local_trace.Run();
}
std::optional<std::string> TraceReportDatabase::GetTraceContent(
const base::Token& uuid) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return std::nullopt;
}
sql::Statement get_local_trace_content(database_.GetCachedStatement(
SQL_FROM_HERE,
"SELECT trace_content FROM local_traces WHERE "
"uuid=?"));
CHECK(get_local_trace_content.is_valid());
get_local_trace_content.BindString(0, uuid.ToString());
if (!get_local_trace_content.Step()) {
return std::nullopt;
}
std::string received_value = get_local_trace_content.ColumnString(0);
if (received_value.empty()) {
return std::nullopt;
}
return received_value;
}
std::optional<std::string> TraceReportDatabase::GetSystemProfile(
const base::Token& uuid) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return std::nullopt;
}
sql::Statement get_system_profile(database_.GetCachedStatement(
SQL_FROM_HERE,
"SELECT system_profile FROM local_traces WHERE "
"uuid=?"));
CHECK(get_system_profile.is_valid());
get_system_profile.BindString(0, uuid.ToString());
if (!get_system_profile.Step()) {
return std::nullopt;
}
std::string received_value = get_system_profile.ColumnString(0);
if (received_value.empty()) {
return std::nullopt;
}
return received_value;
}
bool TraceReportDatabase::DeleteTrace(const base::Token& uuid) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement delete_trace(database_.GetCachedStatement(
SQL_FROM_HERE, "DELETE FROM local_traces WHERE uuid=?"));
CHECK(delete_trace.is_valid());
delete_trace.BindString(0, uuid.ToString());
return delete_trace.Run();
}
bool TraceReportDatabase::DeleteAllTraces() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement delete_all_traces(
database_.GetCachedStatement(SQL_FROM_HERE, "DELETE FROM local_traces"));
CHECK(delete_all_traces.is_valid());
return delete_all_traces.Run();
}
bool TraceReportDatabase::DeleteTracesInDateRange(base::Time start,
base::Time end) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement delete_traces_in_range(database_.GetCachedStatement(
SQL_FROM_HERE,
"DELETE FROM local_traces WHERE creation_time BETWEEN ? AND ?"));
delete_traces_in_range.BindTime(0, start);
delete_traces_in_range.BindTime(1, end);
CHECK(delete_traces_in_range.is_valid());
return delete_traces_in_range.Run();
}
bool TraceReportDatabase::DeleteTraceReportsOlderThan(base::TimeDelta age) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement delete_reports_older_than(
database_.GetCachedStatement(SQL_FROM_HERE, R"sql(
DELETE FROM local_traces
WHERE creation_time < ?)sql"));
delete_reports_older_than.BindTime(0, base::Time(base::Time::Now() - age));
CHECK(delete_reports_older_than.is_valid());
return delete_reports_older_than.Run();
}
bool TraceReportDatabase::DeleteUploadedTraceContentOlderThan(
base::TimeDelta age) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement delete_reports_older_than(
database_.GetCachedStatement(SQL_FROM_HERE, R"sql(
UPDATE local_traces
SET trace_content = null
WHERE state=? AND upload_time < ?)sql"));
delete_reports_older_than.BindInt(
0, static_cast<int>(ReportUploadState::kUploaded));
delete_reports_older_than.BindTime(1, base::Time(base::Time::Now() - age));
CHECK(delete_reports_older_than.is_valid());
return delete_reports_older_than.Run();
}
bool TraceReportDatabase::DeleteOldTraceContent(size_t max_traces) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement delete_old_trace_content(
database_.GetCachedStatement(SQL_FROM_HERE, R"sql(
UPDATE local_traces
SET trace_content = null
WHERE state=? and uuid not in (
SELECT uuid
FROM local_traces
WHERE trace_content IS NOT NULL
ORDER BY creation_time DESC
LIMIT ?)
)sql"));
delete_old_trace_content.BindInt(
0, static_cast<int>(ReportUploadState::kNotUploaded));
delete_old_trace_content.BindInt(1, static_cast<int>(max_traces));
CHECK(delete_old_trace_content.is_valid());
return delete_old_trace_content.Run();
}
bool TraceReportDatabase::AllPendingUploadSkipped(
SkipUploadReason skip_reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return false;
}
sql::Statement statement(
database_.GetCachedStatement(SQL_FROM_HERE,
R"sql(UPDATE local_traces
SET state=?, skip_reason=?
WHERE state=?)sql"));
statement.BindInt(0, static_cast<int>(ReportUploadState::kNotUploaded));
statement.BindInt(1, static_cast<int>(skip_reason));
statement.BindInt(2, static_cast<int>(ReportUploadState::kPending));
CHECK(statement.is_valid());
return statement.Run();
}
bool TraceReportDatabase::EnsureTableCreated() {
DCHECK(database_.is_open());
if (initialized_) {
return true;
}
sql::MetaTable meta_table;
bool has_metatable = meta_table.DoesTableExist(&database_);
bool has_schema = database_.DoesTableExist(kLocalTracesTableName);
if (!has_metatable && has_schema) {
// Existing DB with no meta table. Cannot determine DB version.
if (!database_.Raze()) {
return false;
}
}
if (!meta_table.Init(&database_, kCurrentVersionNumber,
kCurrentVersionNumber)) {
return false;
}
if (meta_table.GetVersionNumber() > kCurrentVersionNumber) {
return false;
}
if (meta_table.GetVersionNumber() < kCurrentVersionNumber) {
if (!database_.Execute("DROP TABLE local_traces")) {
return false;
}
if (!meta_table.SetVersionNumber(kCurrentVersionNumber)) {
return false;
}
}
initialized_ = database_.Execute(kLocalTracesTableSql);
return initialized_;
}
std::vector<ClientTraceReport> TraceReportDatabase::GetAllReports() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::vector<ClientTraceReport> all_reports;
if (!is_initialized()) {
return all_reports;
}
sql::Statement statement(database_.GetCachedStatement(SQL_FROM_HERE, R"sql(
SELECT uuid, creation_time, scenario_name, upload_rule_name,
upload_rule_value, state, upload_time, skip_reason,
trace_content IS NOT NULL as has_trace_content, file_size
FROM local_traces
ORDER BY creation_time DESC
)sql"));
CHECK(statement.is_valid());
while (statement.Step()) {
all_reports.push_back(GetReportFromStatement(statement));
}
return all_reports;
}
std::optional<ClientTraceReport>
TraceReportDatabase::GetNextReportPendingUpload() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!is_initialized()) {
return std::nullopt;
}
sql::Statement statement(database_.GetCachedStatement(SQL_FROM_HERE, R"sql(
SELECT uuid, creation_time, scenario_name, upload_rule_name,
upload_rule_value, state, upload_time, skip_reason,
trace_content IS NOT NULL as has_trace_content, file_size
FROM local_traces WHERE state in (1,2)
ORDER BY creation_time DESC
)sql"));
CHECK(statement.is_valid());
// Select the most recent report first, to prioritize surfacing new
// issues and collecting traces from new scenarios.
while (statement.Step()) {
return GetReportFromStatement(statement);
}
return std::nullopt;
}
std::optional<size_t> TraceReportDatabase::UploadCountSince(
const std::string& scenario_name,
const std::string& upload_rule_name,
base::Time since) {
if (!is_initialized()) {
return std::nullopt;
}
sql::Statement statement(database_.GetCachedStatement(SQL_FROM_HERE, R"sql(
SELECT COUNT(uuid) FROM local_traces
WHERE scenario_name = ? AND upload_rule_name = ? AND creation_time > ?
AND skip_reason=?
)sql"));
statement.BindString(0, scenario_name);
statement.BindString(1, upload_rule_name);
statement.BindTime(2, since);
statement.BindInt(3, static_cast<int>(SkipUploadReason::kNoSkip));
CHECK(statement.is_valid());
while (statement.Step()) {
return static_cast<uint64_t>(statement.ColumnInt64(0));
}
return std::nullopt;
}
base::flat_map<std::string, size_t> TraceReportDatabase::GetScenarioCountsSince(
base::Time since) {
base::flat_map<std::string, size_t> scenario_counts;
if (!is_initialized()) {
return scenario_counts;
}
sql::Statement statement(
database_.GetCachedStatement(SQL_FROM_HERE,
R"sql(SELECT scenario_name, COUNT(uuid) FROM
local_traces
WHERE creation_time > ?
GROUP BY scenario_name)sql"));
statement.BindTime(0, since);
CHECK(statement.is_valid());
while (statement.Step()) {
scenario_counts.emplace(statement.ColumnString(0),
static_cast<uint64_t>(statement.ColumnInt64(1)));
}
return scenario_counts;
}
} // namespace content