blob: b57e931f16dad6cbd60a9cfcde12dc38c1c32289 [file] [log] [blame]
// Copyright 2020 The Fuchsia 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 "src/developer/forensics/feedback_data/system_log_recorder/reader.h"
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <lib/zx/time.h>
#include <algorithm>
#include <cctype>
#include <charconv>
#include <cmath>
#include <sstream>
#include <string>
#include <vector>
#include "src/developer/forensics/feedback_data/constants.h"
#include "src/lib/files/directory.h"
#include "src/lib/files/file.h"
#include "src/lib/files/path.h"
namespace forensics {
namespace feedback_data {
namespace system_log_recorder {
namespace {
// Returns a view starting at the next line in |str| or an empty view if none are left.
std::string_view ToNextLine(std::string_view str) {
const size_t pos = str.find_first_of('\n');
return (pos != str.npos && pos != str.size()) ? str.substr(pos + 1) : std::string_view();
}
// Returns true if |str| starts like a repeated message line.
bool MatchesRepeat(std::string_view str) {
constexpr std::string_view kPrefix(kRepeatedStrPrefix);
if (str.size() < kPrefix.size() || str.substr(0, kPrefix.size()) != kPrefix) {
return false;
}
str.remove_prefix(kPrefix.size());
return !str.empty() && std::isdigit(str.front());
}
// Returns true if |str| starts like a log message.
bool MatchesLogMessage(std::string_view str) {
auto MatchOpenBr = [&str] {
if (str.empty() || str.front() != '[') {
return false;
}
str.remove_prefix(1);
return true;
};
auto MatchDigitsPlus = [&str](const char c) {
std::string_view new_str(str);
while (!new_str.empty() && std::isdigit(new_str.front())) {
new_str.remove_prefix(1);
}
if (new_str == str || (!new_str.empty() && new_str.front() != c)) {
return false;
}
str = new_str.substr(1);
return true;
};
return
// The timestamp is like [$secs.$msecs]
MatchOpenBr() && MatchDigitsPlus('.') && MatchDigitsPlus(']') &&
// The process koid is like [$pid]
MatchOpenBr() && MatchDigitsPlus(']') &&
// The thread koid is like [$tid]
MatchOpenBr() && MatchDigitsPlus(']');
}
// Splits message into 3 parts.
// 1) The portion of the message before the first sequence of repeated warnings.
// 2) The aggregated count of the sequence of repeated warnings.
// 3) The portion of the message after the first sequence of repeated warnings.
//
// Note, 2) and 3) will be default initialized if no repeated warnings are present.
std::tuple<std::string_view, size_t, std::string_view> SplitRepeatedCount(
const std::string_view message) {
constexpr std::string_view kPrefix(kRepeatedStrPrefix);
std::string_view head;
size_t repeated{0};
std::string_view tail(message);
// Find the first line matching the repeated message warning.
while (!tail.empty()) {
if (MatchesRepeat(tail)) {
head = std::string_view(message.data(), tail.data() - message.data());
break;
}
tail = ToNextLine(tail);
}
// No repeated warning found.
if (tail.empty()) {
return {message, 0, std::string_view()};
}
// Aggregate the repeated counts and find the end of the consecutive repeated message warnings.
while (!tail.empty()) {
if (!MatchesRepeat(tail)) {
break;
}
std::string_view new_tail = tail;
new_tail.remove_prefix(kPrefix.size());
if (new_tail.empty() || !std::isdigit(new_tail.front())) {
break;
}
int r{0};
if (std::from_chars(new_tail.data(), new_tail.data() + new_tail.size(), r).ec != std::errc{}) {
FX_LOGS(ERROR) << "Failed to extract repeat count from '" << new_tail << "'";
break;
}
repeated += r;
tail = ToNextLine(new_tail);
}
return {head, repeated, tail};
}
// Returns the messages which make up |log|.
std::vector<std::string_view> ParseMessages(const std::string_view log) {
if (log.empty()) {
return {};
}
// Overallocate to prevent residual memory when growing |messages|.
//
// 20,000 was selected because log files can be large and this is a large enough allocation to
// allow the allocator to decommit the memory backing |messages| when the object is destroyed.
// This is technically dedends implementation details of scudo, but is considered acceptable
// due to the prior residual impact of processing the previous boot log (https://fxbug.dev/42071246).
std::vector<std::string_view> messages;
messages.reserve(20000);
// Consider the first line the start of a new message, regardless of whether or not it matches a
// message.
messages.emplace_back(log.substr(0, 1));
std::string_view remainder = ToNextLine(log.substr(1));
while (!remainder.empty()) {
// Find either the start of the next message in |log| or the end of |log|.
while (!remainder.empty() && !MatchesLogMessage(remainder)) {
remainder = ToNextLine(remainder);
}
// "Grow" the last message's data.
const char* const end = (!remainder.empty()) ? remainder.data() : log.data() + log.size();
messages.back() = std::string_view(messages.back().data(), end - messages.back().data());
if (remainder.empty()) {
break;
}
messages.emplace_back(remainder.substr(0, 1));
remainder.remove_prefix(1);
}
return messages;
}
std::string PostProcess(const std::string_view log) {
std::vector<std::string_view> messages = ParseMessages(log);
if (messages.empty()) {
return "";
}
// Don't sort with the first message if it doesn't have a timestamp.
auto begin = messages.begin();
if (!MatchesLogMessage(*begin)) {
++begin;
}
std::stable_sort(
begin, messages.end(), [](const std::string_view lhs, const std::string_view rhs) {
const std::string_view lhs_timestamp = lhs.substr(lhs.find('['), lhs.find(']'));
const std::string_view rhs_timestamp = rhs.substr(rhs.find('['), rhs.find(']'));
// The timestamp format is "%05d.%03d" so longer strings mean larger timestamps and then
// we only need to compare the strings if the length is the same.
if (lhs_timestamp.size() != rhs_timestamp.size()) {
return lhs_timestamp.size() < rhs_timestamp.size();
}
return lhs_timestamp < rhs_timestamp;
});
std::string final_log;
final_log.reserve(log.size());
// Appends |message| to |final_log| while recursively accumulating the repeated count for each
// message.
std::function<void(std::string_view)> AddMessage = [&final_log,
&AddMessage](const std::string_view message) {
if (message.empty()) {
return;
}
const auto [head, repeat_count, tail] = SplitRepeatedCount(message);
final_log.append(head);
if (repeat_count == 1) {
final_log.append(kRepeatedOnceFormatStr);
} else if (repeat_count > 1) {
final_log.append(kRepeatedStrPrefix);
final_log.append(std::to_string(repeat_count));
final_log.append(kRepeatedStrSuffix);
}
AddMessage(tail);
};
for (const std::string_view message : messages) {
AddMessage(message);
}
return final_log;
}
} // namespace
bool Concatenate(const std::string& logs_dir, const StorageSize max_decompressed_size,
Decoder* decoder, const std::string& output_file_path, float* compression_ratio) {
// Set the default compression to NAN in case Concatenate() fails.
*compression_ratio = NAN;
if (!files::IsDirectory(logs_dir)) {
FX_LOGS(WARNING) << "No previous boot logs found";
return false;
}
std::vector<std::string> file_names;
files::ReadDirContents(logs_dir, &file_names);
// Remove the current directory from the files.
file_names.erase(std::remove(file_names.begin(), file_names.end(), "."), file_names.end());
// Sort the files based on the number the previous writer assigned them. The lower the number,
// the older the file.
std::sort(file_names.begin(), file_names.end(),
[](const std::string& lhs, const std::string& rhs) {
return std::strtoull(lhs.c_str(), nullptr, /*base=*/10) <
std::strtoull(rhs.c_str(), nullptr, /*base=*/10);
});
std::string uncompressed_log;
uncompressed_log.reserve(max_decompressed_size.ToBytes());
std::string buffer;
buffer.reserve(max_decompressed_size.ToBytes());
uint64_t total_compressed_log_size{0};
for (const std::string& fname : file_names) {
const std::string path = files::JoinPath(logs_dir, fname);
if (!files::ReadFileToString(path, &buffer)) {
continue;
}
total_compressed_log_size += buffer.size();
uncompressed_log.append(decoder->Decode(buffer));
}
if (total_compressed_log_size == 0) {
FX_LOGS(WARNING) << "The encoded previous boot log is empty";
return false;
}
if (uncompressed_log.empty()) {
FX_LOGS(WARNING) << "The decoded previous boot log is empty";
return false;
}
// Sort logs and combine messages for repeated logs.
uncompressed_log = PostProcess(uncompressed_log);
if (uncompressed_log.empty()) {
FX_LOGS(WARNING) << "The post-processed previous boot log is empty";
return false;
}
if (!files::WriteFile(output_file_path, uncompressed_log)) {
FX_LOGS(WARNING) << "Could not write the previous boot log file: " << output_file_path;
return false;
}
// Compression ratio rounded up to the next decimal, e.g., 2.54x compression -> 2.6x.
uint32_t decimal_ratio =
((uint32_t)uncompressed_log.size() * 10 - 1) / total_compressed_log_size + 1;
*compression_ratio = ((float)decimal_ratio) / 10.0f;
return true;
}
} // namespace system_log_recorder
} // namespace feedback_data
} // namespace forensics