blob: 087f1002bd8e242f4c444ec6c7b40ee2e91b18bf [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 "util/metrics.h"
#include <sstream>
#include <stack>
#include <boost/algorithm/string/join.hpp>
#include <boost/bind.hpp>
#include <boost/mem_fn.hpp>
#include <boost/math/special_functions/fpclassify.hpp>
#include <gutil/strings/substitute.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/prettywriter.h>
#include "common/logging.h"
#include "util/impalad-metrics.h"
#include "util/json-util.h"
#include "util/pretty-printer.h"
#include "util/webserver.h"
#include "common/names.h"
using namespace impala;
using namespace rapidjson;
using namespace strings;
namespace impala {
template <>
void ToJsonValue<string>(const string& value, const TUnit::type unit,
Document* document, Value* out_val) {
Value val(value.c_str(), document->GetAllocator());
*out_val = val;
}
void Metric::AddStandardFields(Document* document, Value* val) {
Value name(key_.c_str(), document->GetAllocator());
val->AddMember("name", name, document->GetAllocator());
Value desc(description_.c_str(), document->GetAllocator());
val->AddMember("description", desc, document->GetAllocator());
Value metric_value(ToHumanReadable().c_str(), document->GetAllocator());
val->AddMember("human_readable", metric_value, document->GetAllocator());
}
template <typename T, TMetricKind::type metric_kind_t>
void ScalarMetric<T, metric_kind_t>::ToJson(Document* document, Value* val) {
Value container(kObjectType);
AddStandardFields(document, &container);
Value metric_value;
ToJsonValue(GetValue(), TUnit::NONE, document, &metric_value);
container.AddMember("value", metric_value, document->GetAllocator());
Value type_value(PrintThriftEnum(kind()).c_str(), document->GetAllocator());
container.AddMember("kind", type_value, document->GetAllocator());
Value units(PrintThriftEnum(unit()).c_str(), document->GetAllocator());
container.AddMember("units", units, document->GetAllocator());
*val = container;
}
template <typename T, TMetricKind::type metric_kind_t>
void ScalarMetric<T, metric_kind_t>::ToLegacyJson(Document* document) {
Value val;
ToJsonValue(GetValue(), TUnit::NONE, document, &val);
Value key(key_.c_str(), document->GetAllocator());
document->AddMember(key, val, document->GetAllocator());
}
template <typename T, TMetricKind::type metric_kind_t>
TMetricKind::type ScalarMetric<T, metric_kind_t>::ToPrometheus(
string name, stringstream* val, stringstream* metric_kind) {
string metric_type = PrintThriftEnum(kind()).c_str();
// prometheus doesn't support 'property', so ignore it
if (!metric_type.compare("property")) {
return TMetricKind::PROPERTY;
}
if (IsUnitTimeBased(unit())) {
// check if unit its 'TIME_MS','TIME_US' or 'TIME_NS' and convert it to seconds,
// this is because prometheus only supports time format in seconds
*val << ConvertToPrometheusSecs(GetValue(), unit());
} else {
*val << GetValue();
}
// convert metric type to lower case, that's what prometheus expects
std::transform(metric_type.begin(), metric_type.end(), metric_type.begin(), ::tolower);
*metric_kind << "# TYPE " << name << " " << metric_type;
return kind();
}
template <typename T, TMetricKind::type metric_kind_t>
string ScalarMetric<T, metric_kind_t>::ToHumanReadable() {
return PrettyPrinter::Print(GetValue(), unit());
}
MetricDefs* MetricDefs::GetInstance() {
// Note that this is not thread-safe in C++03 (but will be in C++11 see
// http://stackoverflow.com/a/19907903/132034). We don't bother with the double-check
// locking pattern because it introduces complexity whereas a race is very unlikely
// and it doesn't matter if we construct two instances since MetricDefsConstants is
// just a constant map.
static MetricDefs instance;
return &instance;
}
TMetricDef MetricDefs::Get(const string& key, const string& arg) {
MetricDefs* inst = GetInstance();
map<string, TMetricDef>::iterator it = inst->metric_defs_.TMetricDefs.find(key);
if (it == inst->metric_defs_.TMetricDefs.end()) {
DCHECK(false) << "Could not find metric definition for key=" << key << " arg=" << arg;
return TMetricDef();
}
TMetricDef md = it->second;
md.__set_key(Substitute(md.key, arg));
md.__set_description(Substitute(md.description, arg));
return md;
}
MetricGroup::MetricGroup(const string& name)
: obj_pool_(new ObjectPool()), name_(name) { }
Status MetricGroup::Init(Webserver* webserver) {
if (webserver != NULL) {
Webserver::UrlCallback default_callback =
bind<void>(mem_fn(&MetricGroup::CMCompatibleCallback), this, _1, _2);
webserver->RegisterUrlCallback("/jsonmetrics", "legacy-metrics.tmpl",
default_callback, false);
Webserver::UrlCallback json_callback =
bind<void>(mem_fn(&MetricGroup::TemplateCallback), this, _1, _2);
webserver->RegisterUrlCallback("/metrics", "metrics.tmpl", json_callback, true);
Webserver::RawUrlCallback prometheus_callback =
bind<void>(mem_fn(&MetricGroup::PrometheusCallback), this, _1, _2, _3);
webserver->RegisterUrlCallback("/metrics_prometheus", prometheus_callback);
}
return Status::OK();
}
void MetricGroup::CMCompatibleCallback(const Webserver::WebRequest& req,
Document* document) {
const auto& args = req.parsed_args;
// If the request has a 'metric' argument, search all top-level metrics for that metric
// only. Otherwise, return document with list of all metrics at the top level.
Webserver::ArgumentMap::const_iterator metric_name = args.find("metric");
lock_guard<SpinLock> l(lock_);
if (metric_name != args.end()) {
MetricMap::const_iterator metric = metric_map_.find(metric_name->second);
if (metric != metric_map_.end()) {
metric->second->ToLegacyJson(document);
}
return;
}
stack<MetricGroup*> groups;
groups.push(this);
do {
// Depth-first traversal of children to flatten all metrics, which is what was
// expected by CM before we introduced metric groups.
MetricGroup* group = groups.top();
groups.pop();
for (const ChildGroupMap::value_type& child: group->children_) {
groups.push(child.second);
}
for (const MetricMap::value_type& m: group->metric_map_) {
m.second->ToLegacyJson(document);
}
} while (!groups.empty());
}
void MetricGroup::TemplateCallback(const Webserver::WebRequest& req,
Document* document) {
const auto& args = req.parsed_args;
Webserver::ArgumentMap::const_iterator metric_group = args.find("metric_group");
lock_guard<SpinLock> l(lock_);
// If no particular metric group is requested, render this metric group (and all its
// children).
if (metric_group == args.end()) {
Value container;
ToJson(true, document, &container);
document->AddMember("metric_group", container, document->GetAllocator());
return;
}
// Search all metric groups to find the one we're looking for. In the future, we'll
// change this to support path-based resolution of metric groups.
MetricGroup* found_group = NULL;
stack<MetricGroup*> groups;
groups.push(this);
while (!groups.empty() && found_group == NULL) {
// Depth-first traversal of children to flatten all metrics, which is what was
// expected by CM before we introduced metric groups.
MetricGroup* group = groups.top();
groups.pop();
for (const ChildGroupMap::value_type& child: group->children_) {
if (child.first == metric_group->second) {
found_group = child.second;
break;
}
groups.push(child.second);
}
}
if (found_group != NULL) {
Value container;
found_group->ToJson(false, document, &container);
document->AddMember("metric_group", container, document->GetAllocator());
} else {
Value error(Substitute("Metric group $0 not found", metric_group->second).c_str(),
document->GetAllocator());
document->AddMember("error", error, document->GetAllocator());
}
}
void MetricGroup::PrometheusCallback(
const Webserver::WebRequest& req, stringstream* data, HttpStatusCode* response) {
const auto& args = req.parsed_args;
Webserver::ArgumentMap::const_iterator metric_group = args.find("metric_group");
lock_guard<SpinLock> l(lock_);
// If no particular metric group is requested, render this metric group (and all its
// children).
if (metric_group == args.end()) {
Value container;
ToPrometheus(true, data);
}
}
void MetricGroup::ToJson(bool include_children, Document* document, Value* out_val) {
Value metric_list(kArrayType);
for (const MetricMap::value_type& m: metric_map_) {
Value metric_value;
m.second->ToJson(document, &metric_value);
metric_list.PushBack(metric_value, document->GetAllocator());
}
Value container(kObjectType);
container.AddMember("metrics", metric_list, document->GetAllocator());
Value name(name_.c_str(), document->GetAllocator());
container.AddMember("name", name, document->GetAllocator());
if (include_children) {
Value child_groups(kArrayType);
for (const ChildGroupMap::value_type& child: children_) {
Value child_value;
child.second->ToJson(true, document, &child_value);
child_groups.PushBack(child_value, document->GetAllocator());
}
container.AddMember("child_groups", child_groups, document->GetAllocator());
}
*out_val = container;
}
void MetricGroup::ToPrometheus(bool include_children, stringstream* out_val) {
for (auto const& m : metric_map_) {
stringstream metric_value;
stringstream metric_kind;
const string& name = ImpalaToPrometheusName(m.first);
TMetricKind::type metric_type =
m.second->ToPrometheus(name, &metric_value, &metric_kind);
if (metric_type == TMetricKind::SET || metric_type == TMetricKind::PROPERTY) {
// not supported in prometheus
continue;
}
*out_val << "# HELP " << name << " ";
*out_val << m.second->description_;
*out_val << "\n";
*out_val << metric_kind.str();
*out_val << "\n";
// append only if metric type is not stats, set or histogram
if (metric_type != TMetricKind::HISTOGRAM && metric_type != TMetricKind::STATS) {
*out_val << name;
*out_val << " ";
}
*out_val << metric_value.str();
*out_val << "\n";
}
if (include_children) {
Value child_groups(kArrayType);
for (const ChildGroupMap::value_type& child : children_) {
child.second->ToPrometheus(true, out_val);
}
}
}
string MetricGroup::ImpalaToPrometheusName(const string& impala_metric_name) {
string result = impala_metric_name;
// Substitute characters as needed to match prometheus conventions. The string is
// already the right size so we can do this in place.
for (size_t i = 0; i < result.size(); ++i) {
if (result[i] == '.' || result[i] == '-') result[i] = '_';
}
if (result.compare(0, 7, "impala_") != 0) {
result.insert(0, "impala_");
}
return result;
}
MetricGroup* MetricGroup::GetOrCreateChildGroup(const string& name) {
lock_guard<SpinLock> l(lock_);
ChildGroupMap::iterator it = children_.find(name);
if (it != children_.end()) return it->second;
MetricGroup* group = obj_pool_->Add(new MetricGroup(name));
children_[name] = group;
return group;
}
MetricGroup* MetricGroup::FindChildGroup(const string& name) {
lock_guard<SpinLock> l(lock_);
ChildGroupMap::iterator it = children_.find(name);
if (it != children_.end()) return it->second;
return NULL;
}
Metric* MetricGroup::FindMetricForTestingInternal(const string& key) {
stack<MetricGroup*> groups;
groups.push(this);
lock_guard<SpinLock> l(lock_);
do {
MetricGroup* group = groups.top();
groups.pop();
auto it = group->metric_map_.find(key);
if (it != group->metric_map_.end()) return it->second.get();
for (const auto& child : group->children_) {
groups.push(child.second);
}
} while (!groups.empty());
return nullptr;
}
string MetricGroup::DebugString() {
Webserver::WebRequest empty_req;
Document document;
document.SetObject();
TemplateCallback(empty_req, &document);
StringBuffer strbuf;
PrettyWriter<StringBuffer> writer(strbuf);
document.Accept(writer);
return strbuf.GetString();
}
TMetricDef MakeTMetricDef(const string& key, TMetricKind::type kind, TUnit::type unit) {
TMetricDef ret;
ret.__set_key(key);
ret.__set_kind(kind);
ret.__set_units(unit);
return ret;
}
template <typename T>
double ConvertToPrometheusSecs(const T& val, TUnit::type unit) {
double value = val;
if (unit == TUnit::type::TIME_MS) {
value /= 1000;
} else if (unit == TUnit::type::TIME_US) {
value /= 1000000;
} else if (unit == TUnit::type::TIME_NS) {
value /= 1000000000;
}
return value;
}
// Explicitly instantiate the variants that will be used.
template double ConvertToPrometheusSecs<>(const double&, TUnit::type);
template double ConvertToPrometheusSecs<>(const int64_t&, TUnit::type);
template double ConvertToPrometheusSecs<>(const uint64_t&, TUnit::type);
template <>
double ConvertToPrometheusSecs<string>(const string& val, TUnit::type unit) {
DCHECK(false) << "Should not be called for string metrics";
return 0.0;
}
// Explicitly instantiate template classes with parameter combinations that will be used.
// If these classes are instantiated with new parameters, the instantiation must be
// added to this list. This is required because some methods of these classes are
// defined in .cc files and are used from other translation units.
template class LockedMetric<bool, TMetricKind::PROPERTY>;
template class LockedMetric<std::string, TMetricKind::PROPERTY>;
template class LockedMetric<double, TMetricKind::GAUGE>;
template class AtomicMetric<TMetricKind::GAUGE>;
template class AtomicMetric<TMetricKind::COUNTER>;
} // namespace impala