blob: d167250e4225abd5acdd764213d3a0dfb3f45d8e [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "kudu/tools/diagnostics_log_parser.h"
#include <array>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <optional>
#include <string>
#include <utility>
#include <glog/logging.h>
#include <rapidjson/document.h>
#include "kudu/gutil/map-util.h"
#include "kudu/gutil/port.h"
#include "kudu/gutil/strings/join.h"
#include "kudu/gutil/strings/numbers.h"
#include "kudu/gutil/strings/split.h"
#include "kudu/gutil/strings/stringpiece.h"
#include "kudu/gutil/strings/substitute.h"
#include "kudu/util/jsonreader.h" // IWYU pragma: keep
#include "kudu/util/status.h"
using std::array;
using std::cout;
using std::endl;
using std::ifstream;
using std::string;
using strings::Substitute;
namespace kudu {
namespace tools {
const char* RecordTypeToString(RecordType r) {
switch (r) {
case RecordType::kStacks: return "stacks"; break;
case RecordType::kSymbols: return "symbols"; break;
case RecordType::kUnknown: return "<unknown>"; break;
}
return "<unreachable>";
}
std::ostream& operator<<(std::ostream& o, RecordType r) {
return o << RecordTypeToString(r);
}
void StackDumpingLogVisitor::VisitSymbol(const string& addr, const string& symbol) {
InsertIfNotPresent(&symbols_, addr, symbol);
}
void StackDumpingLogVisitor::VisitStacksRecord(const StacksRecord& sr) {
if (!first_) {
cout << endl << endl;
}
first_ = false;
cout << "Stacks at " << sr.date_time << " (" << sr.reason << "):" << endl;
for (const auto& group : sr.groups) {
cout << " tids=["
<< JoinMapped(group.tids, [](int t) { return std::to_string(t); }, ",")
<< "]" << endl;
for (const auto& addr : group.frame_addrs) {
// NOTE: passing 'kUnknownSymbols' as the default instead of a "foo"
// literal is important to avoid capturing a reference to a temporary.
// See the FindWithDefault() docs for details.
const auto& sym = FindWithDefault(symbols_, addr, kUnknownSymbol);
cout << std::setw(20) << addr << " " << sym << endl;
}
}
}
Status ParsedLine::Parse(string line) {
// Take ownership of the line to avoid copying substrings.
line_ = std::move(line);
// Lines have the following format:
//
// I0220 17:38:09.950546 metrics 1519177089950546 <json blob>...
//
if (line_.empty() || line_[0] != 'I') {
return Status::InvalidArgument("lines must start with 'I'");
}
array<StringPiece, 5> fields = strings::Split(
line_, strings::delimiter::Limit(" ", 4));
fields[0].remove_prefix(1); // Remove the 'I'.
// Sanity check the microsecond timestamp.
// Eventually, it should be used when processing metrics records.
int64_t time_us;
if (!safe_strto64(fields[3].data(), fields[3].size(), &time_us)) {
return Status::InvalidArgument("invalid timestamp", fields[3]);
}
// TODO(todd) JsonReader should be able to parse from a StringPiece
// directly instead of making the copy here.
json_.emplace(fields[4].ToString());
Status s = json_->Init();
if (!s.ok()) {
json_.reset();
return s.CloneAndPrepend("invalid JSON payload");
}
date_ = fields[0];
time_ = fields[1];
if (fields[2] == "symbols") {
type_ = RecordType::kSymbols;
} else if (fields[2] == "stacks") {
type_ = RecordType::kStacks;
} else {
type_ = RecordType::kUnknown;
}
return Status::OK();
}
string ParsedLine::date_time() const {
return Substitute("$0 $1", date_, time_);
}
LogParser::LogParser(LogVisitor* visitor)
: visitor_(CHECK_NOTNULL(visitor)) {
}
Status LogParser::ParseLine(string line) {
ParsedLine pl;
RETURN_NOT_OK(pl.Parse(std::move(line)));
switch (pl.type()) {
case RecordType::kSymbols:
RETURN_NOT_OK(ParseSymbols(pl));
break;
case RecordType::kStacks: {
RETURN_NOT_OK(ParseStacks(pl));
break;
}
default:
break;
}
return Status::OK();
}
Status LogParser::ParseSymbols(const ParsedLine& pl) {
CHECK_EQ(RecordType::kSymbols, pl.type());
if (!pl.json()->IsObject()) {
return Status::InvalidArgument("expected symbols data to be a JSON object");
}
for (auto it = pl.json()->MemberBegin();
it != pl.json()->MemberEnd();
++it) {
if (PREDICT_FALSE(!it->value.IsString())) {
return Status::InvalidArgument("expected symbol values to be strings");
}
visitor_->VisitSymbol(it->name.GetString(), it->value.GetString());
}
return Status::OK();
}
Status LogParser::ParseStackGroup(const rapidjson::Value& group_json,
StacksRecord::Group* group) {
DCHECK(group);
StacksRecord::Group ret;
if (PREDICT_FALSE(!group_json.IsObject())) {
return Status::InvalidArgument("expected stacks groups to be JSON objects");
}
if (!group_json.HasMember("tids") || !group_json.HasMember("stack")) {
return Status::InvalidArgument("expected stacks groups to have frames and tids");
}
// Parse the tids.
const auto& tids = group_json["tids"];
if (PREDICT_FALSE(!tids.IsArray())) {
return Status::InvalidArgument("expected 'tids' to be an array");
}
ret.tids.reserve(tids.Size());
for (const auto* tid = tids.Begin();
tid != tids.End();
++tid) {
if (PREDICT_FALSE(!tid->IsNumber())) {
return Status::InvalidArgument("expected 'tids' elements to be numeric");
}
ret.tids.push_back(tid->GetInt64());
}
// Parse and symbolize the stack trace itself.
const auto& stack = group_json["stack"];
if (PREDICT_FALSE(!stack.IsArray())) {
return Status::InvalidArgument("expected 'stack' to be an array");
}
for (const auto* frame = stack.Begin();
frame != stack.End();
++frame) {
if (PREDICT_FALSE(!frame->IsString())) {
return Status::InvalidArgument("expected 'stack' elements to be strings");
}
ret.frame_addrs.emplace_back(frame->GetString());
}
*group = std::move(ret);
return Status::OK();
}
Status LogParser::ParseStacks(const ParsedLine& pl) {
StacksRecord sr;
sr.date_time = pl.date_time();
const rapidjson::Value& json = *pl.json();
if (!json.IsObject()) {
return Status::InvalidArgument("expected stacks data to be a JSON object");
}
// Parse reason if present. If not, we'll just leave it empty.
if (json.HasMember("reason")) {
if (!json["reason"].IsString()) {
return Status::InvalidArgument("expected stacks 'reason' to be a string");
}
sr.reason = json["reason"].GetString();
}
// Parse groups.
if (PREDICT_FALSE(!json.HasMember("groups"))) {
return Status::InvalidArgument("no 'groups' field in stacks object");
}
const auto& groups = json["groups"];
if (!groups.IsArray()) {
return Status::InvalidArgument("'groups' field should be an array");
}
for (const rapidjson::Value* group = groups.Begin();
group != groups.End();
++group) {
StacksRecord::Group g;
RETURN_NOT_OK(ParseStackGroup(*group, &g));
sr.groups.emplace_back(std::move(g));
}
visitor_->VisitStacksRecord(std::move(sr));
return Status::OK();
}
} // namespace tools
} // namespace kudu