blob: 29b1c91578fffb2cf198ad90ad5a325317cddcfd [file]
// Copyright 2026 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/lib/diagnostics/fidl_log_message_parser/test_support.h"
#include <lib/syslog/cpp/log_level.h>
#include <algorithm>
#include <cstring>
#include <optional>
#include <sstream>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/pointer.h>
#include "src/lib/diagnostics/fidl_log_message_parser/log_message.h"
#include "src/lib/fsl/vmo/strings.h"
namespace diagnostics::fidl_log_message_parser::testing {
namespace {
const char kPidLabel[] = "pid";
const char kTidLabel[] = "tid";
const char kFileLabel[] = "file";
const char kLineLabel[] = "line";
const char kTagsLabel[] = "tags";
const char kMessageLabel[] = "value";
const char kVerbosityLabel[] = "verbosity";
inline fuchsia::logger::LogLevelFilter StringToSeverity(const std::string& input) {
if (strcasecmp(input.c_str(), "trace") == 0) {
return fuchsia::logger::LogLevelFilter::TRACE;
}
if (strcasecmp(input.c_str(), "debug") == 0) {
return fuchsia::logger::LogLevelFilter::DEBUG;
}
if (strcasecmp(input.c_str(), "info") == 0) {
return fuchsia::logger::LogLevelFilter::INFO;
}
if (strcasecmp(input.c_str(), "warn") == 0 || strcasecmp(input.c_str(), "warning") == 0) {
return fuchsia::logger::LogLevelFilter::WARN;
}
if (strcasecmp(input.c_str(), "error") == 0) {
return fuchsia::logger::LogLevelFilter::ERROR;
}
if (strcasecmp(input.c_str(), "fatal") == 0) {
return fuchsia::logger::LogLevelFilter::FATAL;
}
return fuchsia::logger::LOG_LEVEL_DEFAULT;
}
std::string GetComponentName(const std::string& moniker) {
size_t pos = moniker.rfind('/');
if (pos == std::string::npos) {
return moniker;
}
if (pos + 1 < moniker.size()) {
return moniker.substr(pos + 1);
}
return moniker;
}
struct ParsedJsonLog {
zx::basic_time<ZX_CLOCK_BOOT> timestamp{0};
int32_t severity{static_cast<int32_t>(fuchsia::logger::LogLevelFilter::INFO)};
uint64_t pid{0};
uint64_t tid{0};
std::vector<std::string> tags;
uint32_t dropped_logs{0};
std::string msg;
std::string file;
std::optional<uint64_t> line;
};
inline fpromise::result<ParsedJsonLog, std::string> ParseSingleJsonLog(rapidjson::Value& value) {
ParsedJsonLog ret;
std::stringstream kv_mapping;
if (!value.IsObject()) {
return fpromise::error("Value is not an object");
}
auto metadata = value.FindMember("metadata");
auto payload = value.FindMember("payload");
if (metadata == value.MemberEnd() || payload == value.MemberEnd() ||
!metadata->value.IsObject() || !payload->value.IsObject()) {
return fpromise::error("Expected metadata and payload objects");
}
auto root = payload->value.FindMember("root");
if (root == payload->value.MemberEnd() || !root->value.IsObject()) {
return fpromise::error("Expected payload.root to be an object if present");
}
auto timestamp = metadata->value.FindMember("timestamp");
if (timestamp == metadata->value.MemberEnd() || !timestamp->value.IsUint64()) {
return fpromise::error("Expected metadata.timestamp key");
}
ret.timestamp = zx::basic_time<ZX_CLOCK_BOOT>(timestamp->value.GetInt64());
auto severity = metadata->value.FindMember("severity");
if (severity == metadata->value.MemberEnd() || !severity->value.IsString()) {
return fpromise::error("Expected metadata.severity key");
}
ret.severity = static_cast<int32_t>(StringToSeverity(severity->value.GetString()));
auto moniker = value.FindMember("moniker");
std::string moniker_string;
if (moniker != value.MemberEnd() && moniker->value.IsString()) {
moniker_string = moniker->value.GetString();
}
uint32_t dropped_logs = 0;
if (metadata->value.HasMember("errors")) {
auto& errors = metadata->value["errors"];
if (errors.IsArray()) {
for (rapidjson::SizeType i = 0; i < errors.Size(); i++) {
auto* val = rapidjson::Pointer("/dropped_logs/count").Get(errors[i]);
if (val && val->IsUint()) {
dropped_logs += val->GetUint();
}
}
}
}
if (payload->value.MemberCount() == 1 && payload->value.HasMember("root")) {
auto root_member = payload->value.FindMember("root");
if (root_member == payload->value.MemberEnd() || !root_member->value.IsObject()) {
return fpromise::error("Expected payload.root to be an object if present");
}
auto msg_member = root_member->value.FindMember("message");
if (msg_member == root_member->value.MemberEnd() || !msg_member->value.IsObject()) {
return fpromise::error("Expected payload.root.message to be an object if present");
}
payload = msg_member;
}
std::string msg;
std::optional<int> verbosity;
for (auto it = payload->value.MemberBegin(); it != payload->value.MemberEnd(); ++it) {
if (!it->name.IsString()) {
return fpromise::error("A key is not a string");
}
std::string name = it->name.GetString();
if (name == kMessageLabel && it->value.IsString()) {
msg = it->value.GetString();
}
if ((name == kVerbosityLabel) && it->value.IsInt()) {
verbosity = it->value.GetInt();
if (ret.severity != verbosity.value_or(ret.severity)) {
ret.severity = static_cast<int32_t>(fidl_log_message_parser::GetSeverityFromVerbosity(
static_cast<uint8_t>(verbosity.value())));
}
}
}
for (auto it = metadata->value.MemberBegin(); it != metadata->value.MemberEnd(); ++it) {
if (!it->name.IsString()) {
return fpromise::error("A key is not a string");
}
std::string name = it->name.GetString();
if (name == kTagsLabel) {
if (it->value.IsString()) {
ret.tags.emplace_back(it->value.GetString());
} else if (it->value.IsArray()) {
for (rapidjson::SizeType i = 0; i < it->value.Size(); ++i) {
auto& val = it->value[i];
if (!val.IsString()) {
return fpromise::error("Tags array must contain strings");
}
ret.tags.emplace_back(val.GetString());
}
} else {
return fpromise::error("Tags must be a string or array of strings");
}
} else if (name == kTidLabel && it->value.IsUint64()) {
ret.tid = it->value.GetUint64();
} else if (name == kPidLabel && it->value.IsUint64()) {
ret.pid = it->value.GetUint64();
} else if (name == kFileLabel && it->value.IsString()) {
ret.file = it->value.GetString();
} else if (name == kLineLabel && it->value.IsUint64()) {
ret.line = it->value.GetUint64();
}
}
auto kvps = root->value.FindMember("keys");
if ((kvps != root->value.MemberEnd()) && kvps->value.IsObject() && (kvps->name == "keys")) {
for (auto it = kvps->value.MemberBegin(); it != kvps->value.MemberEnd(); ++it) {
if (!it->name.IsString()) {
return fpromise::error("A key is not a string");
}
std::string name = it->name.GetString();
kv_mapping << " " << std::move(name) << "=";
if (it->value.IsInt64()) {
kv_mapping << it->value.GetInt64();
} else if (it->value.IsUint64()) {
kv_mapping << it->value.GetUint64();
} else if (it->value.IsDouble()) {
kv_mapping << it->value.GetDouble();
} else if (it->value.IsString()) {
kv_mapping << "\"";
auto str_value = it->value.GetString();
if (strchr(str_value, '"') != nullptr) {
size_t len = strlen(str_value);
for (size_t i = 0; i < len; ++i) {
char c = str_value[i];
if (c == '"') {
kv_mapping << '\\';
}
kv_mapping << c;
}
} else {
kv_mapping << str_value;
}
kv_mapping << "\"";
} else {
kv_mapping << "<unknown>";
}
}
}
if (!ret.file.empty() && ret.line.has_value()) {
std::stringstream enc;
enc << "[" << ret.file << "(" << ret.line.value() << ")] ";
ret.msg = enc.str();
}
ret.msg += msg;
bool needs_add = true;
if (!ret.msg.empty()) {
if (ret.msg[ret.msg.size() - 1] == ' ') {
std::string truncated = kv_mapping.str();
truncated.erase(0, 1);
ret.msg += truncated;
needs_add = false;
}
}
if (needs_add) {
ret.msg += kv_mapping.str();
}
auto component_name = GetComponentName(moniker_string);
if ((ret.tags.empty() && !component_name.empty()) ||
(!ret.tags.empty() &&
(std::find(ret.tags.begin(), ret.tags.end(), component_name) == ret.tags.end()) &&
!component_name.empty())) {
if (component_name != ".") {
ret.tags.emplace(ret.tags.begin(), component_name);
}
}
if (dropped_logs > 0) {
ret.dropped_logs = dropped_logs;
}
return fpromise::ok(std::move(ret));
}
std::vector<uint8_t> BuildFxtRecordFromParsed(const ParsedJsonLog& parsed) {
fuchsia_logging::LogBuffer buffer;
buffer.BeginRecord(parsed.timestamp, static_cast<FuchsiaLogSeverity>(parsed.severity),
std::nullopt, 0, parsed.msg, 0, parsed.pid, parsed.tid);
if (parsed.dropped_logs > 0) {
buffer.WriteKeyValue("num_dropped", static_cast<uint64_t>(parsed.dropped_logs));
}
for (const auto& tag : parsed.tags) {
buffer.WriteKeyValue("tag", tag);
}
auto span = buffer.EndRecord();
return std::vector<uint8_t>(span.begin(), span.end());
}
} // namespace
std::vector<uint8_t> BuildFxtRecord(zx::basic_time<ZX_CLOCK_BOOT> timestamp,
FuchsiaLogSeverity severity, std::string_view message,
zx_koid_t pid, zx_koid_t tid,
const std::vector<std::string>& tags) {
fuchsia_logging::LogBuffer buffer;
buffer.BeginRecord(timestamp, severity, std::nullopt, 0, message, 0, pid, tid);
for (const auto& tag : tags) {
buffer.WriteKeyValue("tag", tag);
}
auto span = buffer.EndRecord();
return std::vector<uint8_t>(span.begin(), span.end());
}
zx::vmo BuildFxtVmo(zx::basic_time<ZX_CLOCK_BOOT> timestamp, FuchsiaLogSeverity severity,
std::string_view message, zx_koid_t pid, zx_koid_t tid,
const std::vector<std::string>& tags) {
auto bytes = BuildFxtRecord(timestamp, severity, message, pid, tid, tags);
zx::vmo vmo;
zx_status_t status = zx::vmo::create(bytes.size(), 0, &vmo);
if (status != ZX_OK) {
return zx::vmo();
}
status = vmo.write(bytes.data(), 0, bytes.size());
if (status != ZX_OK) {
return zx::vmo();
}
status = vmo.set_prop_content_size(bytes.size());
if (status != ZX_OK) {
return zx::vmo();
}
return vmo;
}
zx::vmo EncodeJsonLogToFxtVmo(std::string_view json_str) {
rapidjson::Document doc;
doc.Parse(json_str.data(), json_str.size());
if (doc.HasParseError()) {
return zx::vmo();
}
std::vector<uint8_t> fxt_bytes;
if (doc.IsArray()) {
for (rapidjson::SizeType i = 0; i < doc.Size(); ++i) {
auto parsed = ParseSingleJsonLog(doc[i]);
if (parsed.is_ok()) {
auto record = BuildFxtRecordFromParsed(parsed.value());
fxt_bytes.insert(fxt_bytes.end(), record.begin(), record.end());
}
}
} else if (doc.IsObject()) {
auto parsed = ParseSingleJsonLog(doc);
if (parsed.is_ok()) {
auto record = BuildFxtRecordFromParsed(parsed.value());
fxt_bytes.insert(fxt_bytes.end(), record.begin(), record.end());
}
}
if (fxt_bytes.empty()) {
return zx::vmo();
}
zx::vmo vmo;
zx_status_t status = zx::vmo::create(fxt_bytes.size(), 0, &vmo);
if (status != ZX_OK) {
return zx::vmo();
}
status = vmo.write(fxt_bytes.data(), 0, fxt_bytes.size());
if (status != ZX_OK) {
return zx::vmo();
}
status = vmo.set_prop_content_size(fxt_bytes.size());
if (status != ZX_OK) {
return zx::vmo();
}
return vmo;
}
std::vector<zx::vmo> EncodeJsonBatchToFxtVmos(const std::vector<std::string>& json_batch) {
std::vector<zx::vmo> vmos;
vmos.reserve(json_batch.size());
for (const auto& json_str : json_batch) {
auto vmo = EncodeJsonLogToFxtVmo(json_str);
if (vmo.is_valid()) {
vmos.push_back(std::move(vmo));
}
}
return vmos;
}
fpromise::result<std::vector<fpromise::result<fuchsia::logger::LogMessage, std::string>>,
std::string>
ConvertFormattedContentForTest(fuchsia::diagnostics::FormattedContent content) {
if (content.is_fxt()) {
return fidl_log_message_parser::ConvertFormattedContentToLogMessages(std::move(content));
}
if (!content.is_json()) {
return fpromise::error("Expected json or FXT content");
}
std::string data;
if (!fsl::StringFromVmo(content.json(), &data)) {
return fpromise::error("Failed to read string from VMO");
}
content.json().vmo.reset();
rapidjson::Document d;
d.Parse(std::move(data));
if (d.HasParseError()) {
std::string error = "Failed to parse content as JSON. Offset " +
std::to_string(d.GetErrorOffset()) + ": " +
rapidjson::GetParseError_En(d.GetParseError());
return fpromise::error(std::move(error));
}
if (!d.IsArray()) {
return fpromise::error("Expected content to contain an array");
}
std::vector<fpromise::result<fuchsia::logger::LogMessage, std::string>> output;
for (rapidjson::SizeType i = 0; i < d.Size(); ++i) {
auto parsed = ParseSingleJsonLog(d[i]);
if (parsed.is_error()) {
output.emplace_back(fpromise::error(parsed.error()));
continue;
}
// Encode to FXT and decode through production fidl_log_message_parser.
auto fxt_bytes = BuildFxtRecordFromParsed(parsed.value());
zx::vmo vmo;
zx_status_t status = zx::vmo::create(fxt_bytes.size(), 0, &vmo);
if (status != ZX_OK) {
output.emplace_back(fpromise::error("Failed to create FXT VMO"));
continue;
}
status = vmo.write(fxt_bytes.data(), 0, fxt_bytes.size());
if (status != ZX_OK) {
output.emplace_back(fpromise::error("Failed to write FXT VMO"));
continue;
}
status = vmo.set_prop_content_size(fxt_bytes.size());
if (status != ZX_OK) {
output.emplace_back(fpromise::error("Failed to set content size on FXT VMO"));
continue;
}
fuchsia::diagnostics::FormattedContent fxt_content;
fxt_content.set_fxt(std::move(vmo));
auto decoded_results =
fidl_log_message_parser::ConvertFormattedContentToLogMessages(std::move(fxt_content));
if (decoded_results.is_error()) {
output.emplace_back(fpromise::error(decoded_results.error()));
} else if (decoded_results.value().empty()) {
output.emplace_back(fpromise::error("No messages decoded from FXT"));
} else {
output.emplace_back(std::move(decoded_results.value()[0]));
}
}
return fpromise::ok(std::move(output));
}
} // namespace diagnostics::fidl_log_message_parser::testing