| // Copyright 2020 The Chromium OS 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 "croslog/viewer_plaintext.h" |
| |
| #include <memory> |
| #include <unistd.h> |
| #include <utility> |
| |
| #include "base/files/file_util.h" |
| #include "base/json/string_escape.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| |
| #include "croslog/cursor_util.h" |
| #include "croslog/log_parser_audit.h" |
| #include "croslog/log_parser_syslog.h" |
| #include "croslog/severity.h" |
| |
| namespace croslog { |
| |
| namespace { |
| |
| const char* kLogSources[] = { |
| // Log files from rsyslog: |
| // clang-format off |
| "/var/log/arc.log", |
| "/var/log/boot.log", |
| "/var/log/hammerd.log", |
| "/var/log/messages", |
| "/var/log/net.log", |
| "/var/log/secure", |
| "/var/log/upstart.log", |
| // clang-format on |
| }; |
| |
| const char kAuditLogSources[] = "/var/log/audit/audit.log"; |
| |
| int64_t ToMicrosecondsSinceUnixEpoch(base::Time time) { |
| return (time - base::Time::UnixEpoch()).InMicroseconds(); |
| } |
| |
| } // anonymous namespace |
| |
| ViewerPlaintext::ViewerPlaintext(const croslog::Config& config) |
| : config_(config) { |
| Initialize(); |
| } |
| |
| // For test |
| ViewerPlaintext::ViewerPlaintext(const croslog::Config& config, |
| BootRecords&& boot_logs) |
| : config_(config), boot_records_{std::move(boot_logs)} { |
| Initialize(); |
| } |
| |
| void ViewerPlaintext::Initialize() { |
| if (!config_.grep.empty()) { |
| config_grep_.emplace(config_.grep); |
| if (!config_grep_->ok()) |
| config_grep_.reset(); |
| } |
| |
| if (!config_.after_cursor.empty()) { |
| if (ParseCursor(config_.after_cursor, &config_cursor_time_)) |
| config_cursor_mode_ = CursorMode::NEWER; |
| else |
| LOG(WARNING) << "Invalid cursor format in 'after-cursor' option."; |
| } else if (!config_.cursor.empty()) { |
| if (ParseCursor(config_.cursor, &config_cursor_time_)) |
| config_cursor_mode_ = CursorMode::SAME_AND_NEWER; |
| else |
| LOG(WARNING) << "Invalid cursor format in 'cursor' option."; |
| } |
| |
| config_show_cursor_ = config_.show_cursor && !config_.follow; |
| |
| config_boot_range_.reset(); |
| if (config_.boot.has_value()) { |
| auto range = boot_records_.GetBootRange(*config_.boot); |
| if (range.has_value()) |
| config_boot_range_.emplace(*range); |
| } |
| } |
| |
| bool ViewerPlaintext::Run() { |
| bool install_change_watcher = config_.follow; |
| for (size_t i = 0; i < base::size(kLogSources); i++) { |
| base::FilePath path(kLogSources[i]); |
| if (!base::PathExists(path)) |
| continue; |
| multiplexer_.AddSource(path, std::make_unique<LogParserSyslog>(), |
| install_change_watcher); |
| } |
| |
| if (base::PathExists(base::FilePath(kAuditLogSources))) { |
| multiplexer_.AddSource(base::FilePath(kAuditLogSources), |
| std::make_unique<LogParserAudit>(), |
| install_change_watcher); |
| } |
| |
| multiplexer_.AddObserver(this); |
| |
| if (config_.lines >= 0) { |
| multiplexer_.SetLinesFromLast(config_.lines); |
| } else if (config_.follow) { |
| multiplexer_.SetLinesFromLast(10); |
| } |
| |
| ReadRemainingLogs(); |
| |
| if (config_.follow) { |
| // Wait for file changes. |
| run_loop_.Run(); |
| |
| multiplexer_.RemoveObserver(this); |
| } |
| |
| return true; |
| } |
| |
| void ViewerPlaintext::OnLogFileChanged() { |
| ReadRemainingLogs(); |
| } |
| |
| bool ViewerPlaintext::ShouldFilterOutEntry(const LogEntry& e) { |
| if (config_cursor_mode_ != CursorMode::UNSPECIFIED) { |
| if ((config_cursor_mode_ == CursorMode::NEWER && |
| config_cursor_time_ >= e.time()) || |
| (config_cursor_mode_ == CursorMode::SAME_AND_NEWER && |
| config_cursor_time_ > e.time())) { |
| // TODO(yoshiki): Consider the case that multiple logs have the same |
| // time. |
| return true; |
| } |
| } |
| |
| const std::string& tag = e.tag(); |
| if (!config_.identifier.empty() && config_.identifier != tag) |
| return true; |
| |
| const Severity severity = e.severity(); |
| if (config_.severity != Severity::UNSPECIFIED && config_.severity < severity) |
| return true; |
| |
| const std::string& message = e.message(); |
| if (config_grep_.has_value() && !RE2::PartialMatch(message, *config_grep_)) |
| return true; |
| |
| if (config_.boot.has_value()) { |
| if (!config_boot_range_.has_value() || |
| !config_boot_range_->Contains(e.time())) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| void ViewerPlaintext::ReadRemainingLogs() { |
| base::Time last_shown_log_time; |
| |
| while (true) { |
| const MaybeLogEntry& e = multiplexer_.Forward(); |
| if (!e.has_value()) |
| break; |
| |
| // Shoe the last cursor regardless of visibility. |
| if (config_show_cursor_) |
| last_shown_log_time = e->time(); |
| |
| if (ShouldFilterOutEntry(*e)) |
| continue; |
| |
| WriteLog(*e); |
| } |
| |
| if (config_show_cursor_) { |
| if (last_shown_log_time.is_null()) { |
| multiplexer_.SetLinesFromLast(1); |
| const MaybeLogEntry& e = multiplexer_.Forward(); |
| if (e.has_value()) |
| last_shown_log_time = e->time(); |
| } |
| |
| if (!last_shown_log_time.is_null()) { |
| WriteOutput("-- cursor: "); |
| WriteOutput(GenerateCursor(last_shown_log_time)); |
| WriteOutput("\n", 1); |
| } |
| } |
| } |
| |
| std::vector<std::pair<std::string, std::string>> |
| ViewerPlaintext::GenerateKeyValues(const LogEntry& e) { |
| std::vector<std::pair<std::string, std::string>> kvs; |
| kvs.push_back(std::make_pair( |
| "PRIORITY", base::NumberToString(static_cast<int>(e.severity())))); |
| kvs.push_back(std::make_pair("SYSLOG_IDENTIFIER", e.tag())); |
| |
| const std::string& boot_id = GetBootIdAt(e.time()); |
| if (!boot_id.empty()) |
| kvs.push_back(std::make_pair("_BOOT_ID", boot_id)); |
| |
| std::string timestamp = |
| base::NumberToString(ToMicrosecondsSinceUnixEpoch(e.time())); |
| kvs.push_back(std::make_pair("__REALTIME_TIMESTAMP", timestamp)); |
| kvs.push_back(std::make_pair("_SOURCE_REALTIME_TIMESTAMP", timestamp)); |
| |
| if (e.pid() != -1) { |
| kvs.push_back(std::make_pair("SYSLOG_PID", base::NumberToString(e.pid()))); |
| kvs.push_back(std::make_pair("_PID", base::NumberToString(e.pid()))); |
| } |
| kvs.push_back(std::make_pair("MESSAGE", e.message())); |
| return kvs; |
| } |
| |
| void ViewerPlaintext::WriteLog(const LogEntry& entry) { |
| if (config_.output == OutputMode::EXPORT) |
| return WriteLogInExportFormat(entry); |
| if (config_.output == OutputMode::JSON) |
| return WriteLogInJsonFormat(entry); |
| |
| const std::string& s = entry.entire_line(); |
| WriteOutput(s); |
| WriteOutput("\n", 1); |
| } |
| |
| void ViewerPlaintext::WriteLogInExportFormat(const LogEntry& entry) { |
| const auto&& kvs = GenerateKeyValues(entry); |
| for (const auto& kv : kvs) { |
| WriteOutput(kv.first); |
| WriteOutput("=", 1); |
| WriteOutput(kv.second); |
| WriteOutput("\n", 1); |
| } |
| WriteOutput("\n", 1); |
| } |
| |
| std::string ViewerPlaintext::GetBootIdAt(base::Time time) { |
| const auto& boot_ranges = boot_records_.boot_ranges(); |
| DCHECK_GE(cache_boot_range_index_, -1); |
| DCHECK_LT(cache_boot_range_index_, static_cast<int>(boot_ranges.size())); |
| |
| // First, tries to reuse the index used at the last time. In most case, the |
| // logs are read sequentially and the boot id is likely to be same as the |
| // previous. |
| if (cache_boot_range_index_ != -1 && |
| boot_ranges[cache_boot_range_index_].Contains(time)) { |
| return boot_ranges[cache_boot_range_index_].boot_id(); |
| } |
| |
| // Otherwise, searches the boot id sequentially from the boot log. |
| for (int i = boot_ranges.size() - 1; i >= 0; i--) { |
| const auto& boot_range = boot_ranges[i]; |
| if (boot_range.Contains(time)) { |
| cache_boot_range_index_ = i; |
| return boot_range.boot_id(); |
| } |
| } |
| return base::EmptyString(); |
| } |
| |
| void ViewerPlaintext::WriteLogInJsonFormat(const LogEntry& entry) { |
| const auto&& kvs = GenerateKeyValues(entry); |
| bool first = true; |
| WriteOutput("{", 1); |
| for (const auto& kv : kvs) { |
| std::string escaped_value; |
| bool ret_value = base::EscapeJSONString(kv.second, true, &escaped_value); |
| if (!ret_value) |
| escaped_value = "<<INVALID>>"; |
| |
| if (!first) |
| WriteOutput(", \"", 3); |
| else |
| WriteOutput("\"", 1); |
| // All keys are hard-corded and unnecessary to escape. |
| WriteOutput(kv.first); |
| WriteOutput("\": ", 3); |
| WriteOutput(escaped_value); |
| first = false; |
| } |
| WriteOutput("}\n", 2); |
| } |
| |
| void ViewerPlaintext::WriteOutput(const std::string& str) { |
| WriteOutput(str.data(), str.size()); |
| } |
| |
| void ViewerPlaintext::WriteOutput(const char* str, size_t size) { |
| bool write_stdout_result = |
| base::WriteFileDescriptor(STDOUT_FILENO, str, size); |
| CHECK(write_stdout_result); |
| } |
| |
| } // namespace croslog |