blob: 42c991ae4be1a38eb94ea471069f24119cc0ee58 [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 <lib/zx/stream.h>
#include <lib/zx/vmo.h>
#include <zircon/types.h>
#include <sstream>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/pointer.h>
#include <src/lib/diagnostics/accessor2logger/log_message.h>
#include <src/lib/fsl/vmo/strings.h>
using fuchsia::diagnostics::FormattedContent;
using fuchsia::logger::LogMessage;
namespace diagnostics::accessor2logger {
namespace {
const char kPidLabel[] = "pid";
const char kTidLabel[] = "tid";
const char kTagLabel[] = "tag";
const char kFileLabel[] = "file";
const char kLineLabel[] = "line";
const char kTagsLabel[] = "tags";
const char kMessageLabel[] = "message";
inline int32_t StringToSeverity(const std::string& input) {
if (strcasecmp(input.c_str(), "trace") == 0) {
return static_cast<int32_t>(fuchsia::logger::LogLevelFilter::TRACE);
} else if (strcasecmp(input.c_str(), "debug") == 0) {
return static_cast<int32_t>(fuchsia::logger::LogLevelFilter::DEBUG);
} else if (strcasecmp(input.c_str(), "info") == 0) {
return static_cast<int32_t>(fuchsia::logger::LogLevelFilter::INFO);
} else if (strcasecmp(input.c_str(), "warn") == 0) {
return static_cast<int32_t>(fuchsia::logger::LogLevelFilter::WARN);
} else if (strcasecmp(input.c_str(), "error") == 0) {
return static_cast<int32_t>(fuchsia::logger::LogLevelFilter::ERROR);
} else if (strcasecmp(input.c_str(), "fatal") == 0) {
return static_cast<int32_t>(fuchsia::logger::LogLevelFilter::FATAL);
}
return fuchsia::logger::LOG_LEVEL_DEFAULT;
}
inline fit::result<LogMessage, std::string> JsonToLogMessage(rapidjson::Value& value) {
LogMessage ret = {};
std::stringstream kv_mapping;
if (!value.IsObject()) {
return fit::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 fit::error("Expected metadata and payload objects");
}
auto timestamp = metadata->value.FindMember("timestamp");
if (timestamp == metadata->value.MemberEnd() || !timestamp->value.IsUint64()) {
return fit::error("Expected metadata.timestamp key");
}
ret.time = timestamp->value.GetUint64();
auto severity = metadata->value.FindMember("severity");
if (severity == metadata->value.MemberEnd() || !severity->value.IsString()) {
return fit::error("Expected metadata.severity key");
}
ret.severity = StringToSeverity(severity->value.GetString());
auto moniker = value.FindMember("moniker");
std::string moniker_string;
if (moniker != value.MemberEnd() && moniker->value.IsString()) {
moniker_string = std::move(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();
}
}
}
}
// Flatten payloads containing a "root" node.
// TODO(fxbug.dev/63409): Remove this when "root" is omitted from logs.
if (payload->value.MemberCount() == 1 && payload->value.HasMember("root")) {
payload = payload->value.FindMember("root");
if (!payload->value.IsObject()) {
return fit::error("Expected payload.root to be an object if present");
}
}
std::string msg;
std::string filename;
std::optional<int> line_number;
for (auto it = payload->value.MemberBegin(); it != payload->value.MemberEnd(); ++it) {
if (!it->name.IsString()) {
return fit::error("A key is not a string");
}
std::string name = it->name.GetString();
if (name == kMessageLabel && it->value.IsString()) {
msg = std::move(it->value.GetString());
} else if (name == kTagLabel) {
// TODO(fxbug.dev/63007): Parse only "tags"
if (!it->value.IsString()) {
return fit::error("Tag field must contain a single string value");
}
ret.tags.emplace_back(std::move(it->value.GetString()));
} else if (name == kTagsLabel) {
if (it->value.IsString()) {
ret.tags.emplace_back(std::move(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 fit::error("Tags array must contain strings");
}
ret.tags.emplace_back(std::move(val.GetString()));
}
} else {
return fit::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()) {
filename = it->value.GetString();
} else if (name == kLineLabel && it->value.IsUint64()) {
line_number = it->value.GetUint64();
} else {
// If the name of the field is not a known special field, treat it as a key/value pair and
// append to the message.
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 << std::move(it->value.GetString());
} else {
kv_mapping << "<unknown>";
}
}
}
if (!filename.empty() && line_number.has_value()) {
std::stringstream enc;
enc << "[" << filename << "(" << line_number.value() << ")] ";
ret.msg = enc.str();
}
ret.msg += msg;
ret.msg += kv_mapping.str();
// If there are no tags, automatically tag with the component moniker.
if (ret.tags.size() == 0 && !moniker_string.empty()) {
ret.tags.emplace_back(std::move(moniker_string));
}
if (dropped_logs > 0) {
ret.dropped_logs = dropped_logs;
}
return fit::ok(std::move(ret));
}
} // namespace
fit::result<std::vector<fit::result<fuchsia::logger::LogMessage, std::string>>, std::string>
ConvertFormattedContentToLogMessages(FormattedContent content) {
std::vector<fit::result<LogMessage, std::string>> output;
if (!content.is_json()) {
// Expecting JSON in all cases.
return fit::error("Expected json content");
}
std::string data;
if (!fsl::StringFromVmo(content.json(), &data)) {
return fit::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 fit::error(std::move(error));
}
if (!d.IsArray()) {
return fit::error("Expected content to contain an array");
}
for (rapidjson::SizeType i = 0; i < d.Size(); ++i) {
output.emplace_back(JsonToLogMessage(d[i]));
}
return fit::ok(std::move(output));
}
} // namespace diagnostics::accessor2logger