blob: 18ad9ae7e077175031213e85f7385a787e2f0c8b [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/web_app_logging.h"
#include <string>
#include <string_view>
#include "base/auto_reset.h"
#include "base/barrier_closure.h"
#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/containers/circular_deque.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/i18n/time_formatting.h"
#include "base/json/json_file_value_serializer.h"
#include "base/json/json_writer.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/to_string.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/types/expected_macros.h"
#include "chrome/browser/web_applications/file_utils_wrapper.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/web_contents/web_app_url_loader.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/browser_thread.h"
#include "net/http/http_status_code.h"
namespace web_app {
namespace {
// Heuristic from looking at a number of log entries and averaging their byte
// size.
static constexpr int kEstimatedBytesPerLogEntry = 1 * 1024; // 2KB
// The file size should be large enough that it will not need to rotate often,
// but small enough that it will not get too expensive to re-serialize every
// time the log is flushed.
static constexpr int kMaxLogFileSizeBytes = 2 * 1024 * 1024; // 2MB
static constexpr base::TimeDelta kLogWriteDelay = base::Seconds(5);
// When rotating log files, there can be multiple files that rotate on the same
// date. To create a unique file name, we add an index to the end of the file
// name. This is the maximum index that will be used.
static constexpr int kMaxRotatedLogFileIndex = 100;
int g_max_log_file_size_bytes = kMaxLogFileSizeBytes;
bool IsEmptyIconBitmapsForIconUrl(const IconsMap& icons_map,
const GURL& icon_url) {
IconsMap::const_iterator iter = icons_map.find(icon_url);
if (iter == icons_map.end())
return true;
const std::vector<SkBitmap>& icon_bitmaps = iter->second;
if (icon_bitmaps.empty())
return true;
for (const SkBitmap& icon_bitmap : icon_bitmaps) {
if (!icon_bitmap.isNull() && !icon_bitmap.drawsNothing())
return false;
}
return true;
}
void WritePersistableLogBlocking(scoped_refptr<FileUtilsWrapper> utils,
const base::FilePath& file_path,
base::Value error_log) {
if (!utils->CreateDirectory(file_path.DirName())) {
DLOG(ERROR) << "Could not create directory: " << file_path.DirName();
return;
}
if (!utils->WriteFile(file_path,
base::as_byte_span(error_log.DebugString()))) {
DLOG(ERROR) << "Could not write log file: " << file_path;
}
}
std::optional<base::ListValue> ReadLogFileBlocking(
scoped_refptr<FileUtilsWrapper> utils,
const base::FilePath& log_file) {
if (!utils->PathExists(log_file)) {
return std::nullopt;
}
std::string contents;
if (!utils->ReadFileToString(log_file, &contents)) {
return std::nullopt;
}
return base::JSONReader::ReadList(
contents, base::JSONParserOptions::JSON_ALLOW_TRAILING_COMMAS);
}
void RotateLogFileBlocking(scoped_refptr<FileUtilsWrapper> utils,
const base::FilePath& current_log_file,
const base::Clock* clock) {
if (!utils->PathExists(current_log_file)) {
DLOG(ERROR) << "Current log file does not exist: " << current_log_file;
return;
}
std::optional<base::FilePath> rotated_filename;
std::string date_str =
base::UnlocalizedTimeFormatWithPattern(clock->Now(), "y-MM-dd");
for (int free_index = -1; free_index < kMaxRotatedLogFileIndex;
++free_index) {
std::string suffix = date_str;
if (free_index >= 0) {
base::StrAppend(&suffix, {".", base::NumberToString(free_index)});
}
base::FilePath candidate_filename =
current_log_file.InsertBeforeExtensionASCII(suffix);
if (!utils->PathExists(candidate_filename)) {
rotated_filename = candidate_filename;
break;
}
}
if (!rotated_filename.has_value()) {
DLOG(ERROR) << "Could not find a free index for rotated log file.";
return;
}
if (!utils->Move(current_log_file, *rotated_filename)) {
LOG(ERROR) << "Could not move log file: " << current_log_file.value()
<< " to " << rotated_filename.value();
}
}
} // namespace
// static
base::FilePath PersistableLog::GetLogPath(Profile* profile,
std::string_view log_filename) {
return GetWebAppsRootDirectory(profile).AppendASCII("Logs").AppendASCII(
log_filename);
}
// static
int PersistableLog::GetMaxInMemoryLogEntries() {
static constexpr int kMaxInMemoryLogEntriesWithDebugInfoEnabled = 1000;
static constexpr int kMaxInMemoryLogEntriesWithDebugInfoDisabled = 20;
return base::FeatureList::IsEnabled(features::kRecordWebAppDebugInfo)
? kMaxInMemoryLogEntriesWithDebugInfoEnabled
: kMaxInMemoryLogEntriesWithDebugInfoDisabled;
}
// static
PersistableLogMode PersistableLog::GetMode() {
return base::FeatureList::IsEnabled(features::kRecordWebAppDebugInfo)
? PersistableLogMode::kPersistToDisk
: PersistableLogMode::kInMemory;
}
// static
base::AutoReset<int> PersistableLog::SetMaxLogFileSizeBytesForTesting(
int size) {
CHECK_IS_TEST();
return base::AutoReset<int>(&g_max_log_file_size_bytes, size);
}
std::unique_ptr<PersistableLog> PersistableLog::Create(
const base::FilePath& log_file,
PersistableLogMode mode,
int max_log_entries_in_memory,
scoped_refptr<FileUtilsWrapper> file_utils) {
return base::WrapUnique(new PersistableLog(
log_file, mode, max_log_entries_in_memory, std::move(file_utils),
base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN}),
base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
base::DefaultClock::GetInstance()));
}
std::unique_ptr<PersistableLog> PersistableLog::CreateForTesting(
const base::FilePath& log_file,
PersistableLogMode mode,
int max_log_entries_in_memory,
scoped_refptr<FileUtilsWrapper> file_utils,
scoped_refptr<base::SequencedTaskRunner> log_writing_task_runner,
scoped_refptr<base::SequencedTaskRunner> log_deletion_task_runner,
const base::Clock* clock) {
return base::WrapUnique(new PersistableLog(
log_file, mode, max_log_entries_in_memory, std::move(file_utils),
std::move(log_writing_task_runner), std::move(log_deletion_task_runner),
clock));
}
PersistableLog::PersistableLog(
const base::FilePath& log_file,
PersistableLogMode mode,
int max_log_entries_in_memory,
scoped_refptr<FileUtilsWrapper> file_utils,
scoped_refptr<base::SequencedTaskRunner> log_writing_task_runner,
scoped_refptr<base::SequencedTaskRunner> log_deletion_task_runner,
const base::Clock* clock)
: log_file_(log_file),
mode_(mode),
file_utils_(std::move(file_utils)),
max_log_entries_in_memory_(max_log_entries_in_memory),
log_writing_task_runner_(std::move(log_writing_task_runner)),
log_deletion_task_runner_(std::move(log_deletion_task_runner)),
clock_(CHECK_DEREF(clock)),
log_write_timer_(FROM_HERE,
kLogWriteDelay,
this,
&PersistableLog::MaybeWriteCurrentLog) {
CHECK(!log_file_.empty());
CHECK(file_utils_);
CHECK(log_file_.IsAbsolute());
CHECK(!log_file_.Extension().empty());
switch (mode) {
case PersistableLogMode::kInMemory:
// For in-memory, we only do one file-related task here to delete old
// logs.
// TODO(crbug.com/40224498): Clean up logs older than X days on startup.
log_deletion_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
base::IgnoreResult(&FileUtilsWrapper::DeleteFileRecursively),
file_utils_, log_file_.DirName()));
// Signal the load complete event so that Append() can add to the
// in-memory log.
on_load_complete_.Signal();
return;
case PersistableLogMode::kPersistToDisk:
log_writing_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&ReadLogFileBlocking, file_utils_, log_file_),
base::BindOnce(&PersistableLog::OnLatestLogLoaded,
weak_ptr_factory_.GetWeakPtr()));
break;
}
}
PersistableLog::~PersistableLog() {
MaybeWriteCurrentLog();
}
void PersistableLog::Append(base::DictValue object) {
if (!object.contains("timestamp_ms")) {
object.Set("timestamp_ms",
base::saturated_cast<int>(
clock_->Now().ToDeltaSinceWindowsEpoch().InMilliseconds()));
}
#if DCHECK_IS_ON()
// This is wrapped with DCHECK_IS_ON() to prevent calling DebugString() in
// production builds.
DVLOG(1) << (on_load_complete_.is_signaled() ? "" : "[LOADING] ")
<< log_file_.BaseName() << ": " << object.DebugString();
#endif
if (!on_load_complete_.is_signaled()) {
on_load_complete_.Post(
FROM_HERE,
base::BindOnce(&PersistableLog::Append, weak_ptr_factory_.GetWeakPtr(),
std::move(object)));
return;
}
log_.push_front(object.Clone());
if (base::saturated_cast<int>(log_.size()) > max_log_entries_in_memory_) {
log_.resize(max_log_entries_in_memory_);
}
current_log_for_disk_.Append(std::move(object));
if (mode_ == PersistableLogMode::kPersistToDisk) {
log_write_timer_.Reset();
// Technically we can log frequently for a while and never write to disk, as
// the timer won't trigger. To mitigate, flush if we reach 2x the max log
// size.
if (base::saturated_cast<int>(current_log_for_disk_.size()) *
kEstimatedBytesPerLogEntry * 2 >=
g_max_log_file_size_bytes) {
MaybeWriteCurrentLog();
}
}
}
void PersistableLog::AppendValue(base::Value object) {
Append(base::DictValue().Set("value", std::move(object)));
}
const base::circular_deque<base::DictValue>& PersistableLog::GetEntries()
const {
return log_;
}
base::ListValue PersistableLog::CloneToList() const {
base::ListValue log_entries = base::ListValue::with_capacity(log_.size());
for (const base::DictValue& entry : log_) {
log_entries.Append(entry.Clone());
}
return log_entries;
}
void PersistableLog::OnLatestLogLoaded(
std::optional<base::ListValue> loaded_log) {
if (loaded_log.has_value()) {
current_log_for_disk_ = loaded_log->Clone();
// Note: Items are added to the front as the in-memory log order is from
// newest to oldest.
log_.reserve(current_log_for_disk_.size());
for (base::Value& value : current_log_for_disk_) {
// Ignore non-dict entries, which are not expected.
if (!value.is_dict()) {
continue;
}
log_.push_front(value.GetDict().Clone());
}
}
on_load_complete_.Signal();
}
void PersistableLog::MaybeWriteCurrentLog() {
if (mode_ != PersistableLogMode::kPersistToDisk) {
return;
}
// The log is written, in full, every time this method is called. This is a
// tradeoff between using an easy-to-read and easy-to-write format like JSON,
// which requires re-serialization fully every write (as you cannot simply
// easily append with our built-in methods), and the cost of rewriting the
// whole file. Since this is only used for debugging for users who have
// enabled the debug info flag, the costs here seem acceptable, especially
// since the total log size is limited, and we only write 5 seconds after the
// last log entry is added.
base::OnceClosure log_write_task =
base::BindOnce(&WritePersistableLogBlocking, file_utils_, log_file_,
base::Value(current_log_for_disk_.Clone()));
// Possibly do a log rotation.
if (current_disk_log_entry_count_ * kEstimatedBytesPerLogEntry >
g_max_log_file_size_bytes) {
log_write_task = base::BindOnce(&RotateLogFileBlocking, file_utils_,
log_file_, base::Unretained(&clock_.get()))
.Then(std::move(log_write_task));
current_log_for_disk_.clear();
}
log_writing_task_runner_->PostTask(FROM_HERE, std::move(log_write_task));
current_disk_log_entry_count_ = current_log_for_disk_.size();
}
base::DictValue LogDownloadedIconsErrors(
IconsDownloadedResult icons_downloaded_result,
const IconsMap& icons_map,
const DownloadedIconsHttpResults& icons_http_results) {
base::DictValue icon_errors;
{
// Reports errors only, omits successful entries.
base::Value::List icons_http_errors;
for (const auto& url_and_http_code : icons_http_results) {
const GURL& icon_url = url_and_http_code.first.url;
int http_status_code = url_and_http_code.second;
std::string_view http_code_desc =
net::GetHttpReasonPhrase(http_status_code);
// If the SkBitmap for`icon_url` is missing in `icons_map` then we report
// this miss as an error, even for net::HttpStatusCode::HTTP_OK.
if (IsEmptyIconBitmapsForIconUrl(icons_map, icon_url)) {
base::DictValue icon_http_error;
icon_http_error.Set("icon_url", icon_url.spec());
icon_http_error.Set("icon_size",
url_and_http_code.first.size.ToString());
icon_http_error.Set("http_status_code", http_status_code);
icon_http_error.Set("http_code_desc", http_code_desc);
icons_http_errors.Append(std::move(icon_http_error));
}
}
if (icons_downloaded_result != IconsDownloadedResult::kCompleted ||
!icons_http_errors.empty()) {
icon_errors.Set("icons_downloaded_result",
IconsDownloadedResultToString(icons_downloaded_result));
}
if (!icons_http_errors.empty())
icon_errors.Set("icons_http_results", std::move(icons_http_errors));
}
return icon_errors;
}
} // namespace web_app