blob: 31d12be1adb94ada0920de8f958945519b17a352 [file] [log] [blame]
#include <algorithm>
#include <iostream>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "absl/strings/escaping.h"
#include "gflags/gflags.h"
#include "aos/aos_cli_utils.h"
#include "aos/configuration.h"
#include "aos/events/logging/log_reader.h"
#include "aos/events/simulated_event_loop.h"
#include "aos/init.h"
#include "aos/json_to_flatbuffer.h"
#include "aos/sha256.h"
DEFINE_string(
name, "",
"Name to match for printing out channels. Empty means no name filter.");
DEFINE_string(type, "",
"Channel type to match for printing out channels. Empty means no "
"type filter.");
DEFINE_bool(json, false, "If true, print fully valid JSON");
DEFINE_bool(fetch, false,
"If true, also print out the messages from before the start of the "
"log file");
DEFINE_bool(raw, false,
"If true, just print the data out unsorted and unparsed");
DEFINE_string(raw_header, "",
"If set, the file to read the header from in raw mode");
DEFINE_bool(distributed_clock, false,
"If true, print out the distributed time");
DEFINE_bool(format_raw, true,
"If true and --raw is specified, print out raw data, but use the "
"schema to format the data.");
DEFINE_int64(max_vector_size, 100,
"If positive, vectors longer than this will not be printed");
DEFINE_bool(pretty, false,
"If true, pretty print the messages on multiple lines");
DEFINE_bool(
pretty_max, false,
"If true, expand every field to its own line (expands more than -pretty)");
DEFINE_bool(print_timestamps, true, "If true, timestamps are printed.");
DEFINE_bool(print, true,
"If true, actually print the messages. If false, discard them, "
"confirming they can be parsed.");
DEFINE_uint64(
count, 0,
"If >0, log_cat will exit after printing this many messages. This "
"includes messages from before the start of the log if --fetch is set.");
DEFINE_bool(print_parts_only, false,
"If true, only print out the results of logfile sorting.");
DEFINE_bool(channels, false,
"If true, print out all the configured channels for this log.");
DEFINE_double(monotonic_start_time, 0.0,
"If set, only print messages sent at or after this many seconds "
"after epoch.");
DEFINE_double(monotonic_end_time, 0.0,
"If set, only print messages sent at or before this many seconds "
"after epoch.");
DEFINE_bool(hex, false,
"Are integers in the messages printed in hex notation.");
using aos::monotonic_clock;
namespace chrono = std::chrono;
// Prints out raw log parts to stdout.
int PrintRaw(int argc, char **argv) {
if (argc == 1) {
CHECK(!FLAGS_raw_header.empty());
aos::logger::MessageReader raw_header_reader(FLAGS_raw_header);
std::cout << aos::FlatbufferToJson(raw_header_reader.raw_log_file_header(),
{.multi_line = FLAGS_pretty,
.max_vector_size = static_cast<size_t>(
FLAGS_max_vector_size)})
<< std::endl;
return 0;
}
if (argc != 2 && argc != 1) {
LOG(FATAL) << "Expected 1 logfile as an argument.";
}
aos::logger::SpanReader reader(argv[1]);
absl::Span<const uint8_t> raw_log_file_header_span = reader.ReadMessage();
if (raw_log_file_header_span.empty()) {
LOG(WARNING) << "Empty log file on " << reader.filename();
return 0;
}
// Now, reproduce the log file header deduplication logic inline so we can
// print out all the headers we find.
aos::SizePrefixedFlatbufferVector<aos::logger::LogFileHeader> log_file_header(
raw_log_file_header_span);
if (!log_file_header.Verify()) {
LOG(ERROR) << "Header corrupted on " << reader.filename();
return 1;
}
while (true) {
absl::Span<const uint8_t> maybe_header_data = reader.PeekMessage();
if (maybe_header_data.empty()) {
break;
}
aos::SizePrefixedFlatbufferSpan<aos::logger::LogFileHeader> maybe_header(
maybe_header_data);
if (maybe_header.Verify()) {
std::cout << aos::FlatbufferToJson(
log_file_header, {.multi_line = FLAGS_pretty,
.max_vector_size = static_cast<size_t>(
FLAGS_max_vector_size)})
<< std::endl;
LOG(WARNING) << "Found duplicate LogFileHeader in " << reader.filename();
log_file_header =
aos::SizePrefixedFlatbufferVector<aos::logger::LogFileHeader>(
maybe_header_data);
reader.ConsumeMessage();
} else {
break;
}
}
// And now use the final sha256 to match the raw_header.
std::optional<aos::logger::MessageReader> raw_header_reader;
const aos::logger::LogFileHeader *full_header = &log_file_header.message();
if (!FLAGS_raw_header.empty()) {
raw_header_reader.emplace(FLAGS_raw_header);
std::cout << aos::FlatbufferToJson(full_header,
{.multi_line = FLAGS_pretty,
.max_vector_size = static_cast<size_t>(
FLAGS_max_vector_size)})
<< std::endl;
CHECK_EQ(full_header->configuration_sha256()->string_view(),
aos::Sha256(raw_header_reader->raw_log_file_header().span()));
full_header = raw_header_reader->log_file_header();
}
if (!FLAGS_print) {
return 0;
}
std::cout << aos::FlatbufferToJson(full_header,
{.multi_line = FLAGS_pretty,
.max_vector_size = static_cast<size_t>(
FLAGS_max_vector_size)})
<< std::endl;
CHECK(full_header->has_configuration())
<< ": Missing configuration! You may want to provide the path to the "
"logged configuration file using the --raw_header flag.";
while (true) {
const aos::SizePrefixedFlatbufferSpan<aos::logger::MessageHeader> message(
reader.ReadMessage());
if (message.span().empty()) {
break;
}
CHECK(message.Verify());
const auto *const channels = full_header->configuration()->channels();
const size_t channel_index = message.message().channel_index();
CHECK_LT(channel_index, channels->size());
const aos::Channel *const channel = channels->Get(channel_index);
CHECK(message.Verify()) << absl::BytesToHexString(
std::string_view(reinterpret_cast<const char *>(message.span().data()),
message.span().size()));
if (message.message().data() != nullptr) {
CHECK(channel->has_schema());
CHECK(flatbuffers::Verify(
*channel->schema(), *channel->schema()->root_table(),
message.message().data()->data(), message.message().data()->size()))
<< ": Corrupted flatbuffer on " << channel->name()->c_str() << " "
<< channel->type()->c_str();
}
if (FLAGS_format_raw && message.message().data() != nullptr) {
std::cout << aos::configuration::StrippedChannelToString(channel) << " "
<< aos::FlatbufferToJson(message, {.multi_line = FLAGS_pretty,
.max_vector_size = 4})
<< ": "
<< aos::FlatbufferToJson(
channel->schema(), message.message().data()->data(),
{FLAGS_pretty,
static_cast<size_t>(FLAGS_max_vector_size)})
<< std::endl;
} else {
std::cout << aos::configuration::StrippedChannelToString(channel) << " "
<< aos::FlatbufferToJson(
message, {FLAGS_pretty,
static_cast<size_t>(FLAGS_max_vector_size)})
<< std::endl;
}
}
return 0;
}
// This class prints out all data from a node on a boot.
class NodePrinter {
public:
NodePrinter(aos::EventLoop *event_loop,
aos::SimulatedEventLoopFactory *factory, aos::Printer *printer)
: factory_(factory),
node_factory_(factory->GetNodeEventLoopFactory(event_loop->node())),
event_loop_(event_loop),
node_name_(
event_loop_->node() == nullptr
? ""
: std::string(event_loop->node()->name()->string_view())),
printer_(printer) {
event_loop_->SkipTimingReport();
event_loop_->SkipAosLog();
const flatbuffers::Vector<flatbuffers::Offset<aos::Channel>> *channels =
event_loop_->configuration()->channels();
const monotonic_clock::time_point start_time =
(FLAGS_monotonic_start_time == 0.0
? monotonic_clock::min_time
: monotonic_clock::time_point(
std::chrono::duration_cast<monotonic_clock::duration>(
std::chrono::duration<double>(
FLAGS_monotonic_start_time))));
const monotonic_clock::time_point end_time =
(FLAGS_monotonic_end_time == 0.0
? monotonic_clock::max_time
: monotonic_clock::time_point(
std::chrono::duration_cast<monotonic_clock::duration>(
std::chrono::duration<double>(
FLAGS_monotonic_end_time))));
for (flatbuffers::uoffset_t i = 0; i < channels->size(); i++) {
const aos::Channel *channel = channels->Get(i);
const flatbuffers::string_view name = channel->name()->string_view();
const flatbuffers::string_view type = channel->type()->string_view();
if (name.find(FLAGS_name) != std::string::npos &&
type.find(FLAGS_type) != std::string::npos) {
if (!aos::configuration::ChannelIsReadableOnNode(channel,
event_loop_->node())) {
continue;
}
VLOG(1) << "Listening on " << name << " " << type;
CHECK_NOTNULL(channel->schema());
event_loop_->MakeRawWatcher(channel, [this, channel, start_time,
end_time](
const aos::Context &context,
const void * /*message*/) {
if (!FLAGS_print) {
return;
}
if (FLAGS_count > 0 && printer_->message_count() >= FLAGS_count) {
return;
}
if (!FLAGS_fetch && !started_) {
return;
}
if (context.monotonic_event_time < start_time ||
context.monotonic_event_time > end_time) {
return;
}
printer_->PrintMessage(node_name_, node_factory_, channel, context);
if (FLAGS_count > 0 && printer_->message_count() >= FLAGS_count) {
factory_->Exit();
}
});
}
}
}
void SetStarted(bool started, aos::monotonic_clock::time_point monotonic_now,
aos::realtime_clock::time_point realtime_now) {
started_ = started;
if (FLAGS_json) {
return;
}
if (started_) {
std::cout << std::endl;
std::cout << (event_loop_->node() != nullptr
? (event_loop_->node()->name()->str() + " ")
: "")
<< "Log starting at " << realtime_now << " (" << monotonic_now
<< ")";
std::cout << std::endl << std::endl;
} else {
std::cout << std::endl;
std::cout << (event_loop_->node() != nullptr
? (event_loop_->node()->name()->str() + " ")
: "")
<< "Log shutting down at " << realtime_now << " ("
<< monotonic_now << ")";
std::cout << std::endl << std::endl;
}
}
private:
struct MessageInfo {
std::string node_name;
std::unique_ptr<aos::RawFetcher> fetcher;
};
aos::SimulatedEventLoopFactory *factory_;
aos::NodeEventLoopFactory *node_factory_;
aos::EventLoop *event_loop_;
std::string node_name_;
bool started_ = false;
aos::Printer *printer_ = nullptr;
};
int main(int argc, char **argv) {
gflags::SetUsageMessage(
"Usage:\n"
" log_cat [args] logfile1 logfile2 ...\n"
"\n"
"This program provides a basic interface to dump data from a logfile to "
"stdout. Given a logfile, channel name filter, and type filter, it will "
"print all the messages in the logfile matching the filters. The message "
"filters work by taking the values of --name and --type and printing any "
"channel whose name contains --name as a substr and whose type contains "
"--type as a substr. Not specifying --name or --type leaves them free. "
"Calling this program without --name or --type specified prints out all "
"the logged data.");
aos::InitGoogle(&argc, &argv);
if (FLAGS_raw) {
return PrintRaw(argc, argv);
}
if (argc < 2) {
LOG(FATAL) << "Expected at least 1 logfile as an argument.";
}
const std::vector<aos::logger::LogFile> logfiles =
aos::logger::SortParts(aos::logger::FindLogs(argc, argv));
for (auto &it : logfiles) {
VLOG(1) << it;
if (FLAGS_print_parts_only) {
std::cout << it << std::endl;
}
}
if (FLAGS_print_parts_only) {
return 0;
}
aos::logger::LogReader reader(logfiles);
if (FLAGS_channels) {
const aos::Configuration *config = reader.configuration();
for (const aos::Channel *channel : *config->channels()) {
std::cout << channel->name()->c_str() << " " << channel->type()->c_str()
<< '\n';
}
return 0;
}
{
bool found_channel = false;
const flatbuffers::Vector<flatbuffers::Offset<aos::Channel>> *channels =
reader.configuration()->channels();
for (flatbuffers::uoffset_t i = 0; i < channels->size(); i++) {
const aos::Channel *channel = channels->Get(i);
const flatbuffers::string_view name = channel->name()->string_view();
const flatbuffers::string_view type = channel->type()->string_view();
if (name.find(FLAGS_name) != std::string::npos &&
type.find(FLAGS_type) != std::string::npos) {
found_channel = true;
}
}
if (!found_channel) {
LOG(FATAL) << "Could not find any channels";
}
}
aos::Printer printer(
{
.pretty = FLAGS_pretty,
.max_vector_size = static_cast<size_t>(FLAGS_max_vector_size),
.pretty_max = FLAGS_pretty_max,
.print_timestamps = FLAGS_print_timestamps,
.json = FLAGS_json,
.distributed_clock = FLAGS_distributed_clock,
.hex = FLAGS_hex,
},
false);
std::vector<NodePrinter *> printers;
printers.resize(aos::configuration::NodesCount(reader.configuration()),
nullptr);
aos::SimulatedEventLoopFactory event_loop_factory(reader.configuration());
reader.RegisterWithoutStarting(&event_loop_factory);
for (const aos::Node *node :
aos::configuration::GetNodes(event_loop_factory.configuration())) {
size_t node_index = aos::configuration::GetNodeIndex(
event_loop_factory.configuration(), node);
// Spin up the printer, and hook up the SetStarted method so that it gets
// notified when the log starts and stops.
aos::NodeEventLoopFactory *node_factory =
event_loop_factory.GetNodeEventLoopFactory(node);
node_factory->OnStartup(
[&event_loop_factory, node_factory, &printer, &printers, node_index]() {
printers[node_index] = node_factory->AlwaysStart<NodePrinter>(
"printer", &event_loop_factory, &printer);
});
node_factory->OnShutdown(
[&printers, node_index]() { printers[node_index] = nullptr; });
reader.OnStart(node, [&printers, node_index, node_factory]() {
CHECK(printers[node_index]);
printers[node_index]->SetStarted(true, node_factory->monotonic_now(),
node_factory->realtime_now());
});
reader.OnEnd(node, [&printers, node_index, node_factory]() {
CHECK(printers[node_index]);
printers[node_index]->SetStarted(false, node_factory->monotonic_now(),
node_factory->realtime_now());
});
}
event_loop_factory.Run();
reader.Deregister();
return 0;
}