blob: 2288bd2493494794f1f5f346ef45b04fb8e90558 [file]
// 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 <algorithm> // std::adjacent_find
#include <functional> // std::greater_equal
#include <iterator> // std::next
#include <string> // std::string
#include "butil/float_util.h" // butil::IsFinite
#include "butil/logging.h" // LOG
#include "butil/strings/string_number_conversions.h" // butil::DoubleToString
#include "bvar/histogram.h"
namespace bvar {
// The +Inf/-Inf/NaN of the prometheus text format are not json, so describe()
// cannot borrow that spelling: it writes a non representable number as `null`,
// which is what JSON.stringify() does with one. Finite values still go through
// prometheus_double_to_string() so that the two outputs agree digit for digit
// and neither follows LC_NUMERIC.
static std::string json_double_to_string(double value) {
if (BAIDU_UNLIKELY(!butil::IsFinite(value))) {
return "null";
}
return detail::prometheus_double_to_string(value);
}
Histogram::BucketSchema::BucketSchema(std::initializer_list<double> bounds)
: _bounds(bounds) {
validate_bounds();
}
Histogram::BucketSchema::BucketSchema(const std::vector<double>& bounds)
: _bounds(bounds) {
validate_bounds();
}
void Histogram::BucketSchema::validate_bounds() {
for (auto it = _bounds.begin(); it != _bounds.end();) {
if (!butil::IsFinite(*it)) {
LOG(ERROR) << "Bucket bounds must be finite, dropping " << *it;
it = _bounds.erase(it);
} else {
++it;
}
}
// Drop the bounds that do not keep the sequence strictly ascending.
auto it = _bounds.begin();
while ((it = std::adjacent_find(it, _bounds.end(),
std::greater_equal<>())) != _bounds.end()) {
auto bad = std::next(it);
LOG(ERROR) << "Bucket bounds must be strictly ascending, dropping "
<< *bad << " which is not greater than " << *it;
_bounds.erase(bad);
}
if (_bounds.size() >= MAX_HISTOGRAM_BUCKETS) {
// Shrinking a vector drops the tail, keeping the tightest bounds.
LOG(ERROR) << "A histogram takes at most " << MAX_HISTOGRAM_BUCKETS - 1
<< " bounds, dropping the "
<< _bounds.size() - (MAX_HISTOGRAM_BUCKETS - 1)
<< " ones past the limit";
_bounds.resize(MAX_HISTOGRAM_BUCKETS - 1);
}
if (_bounds.empty()) {
// A schema without any bound would send everything into the +Inf
// bucket, which no quantile can be read off. Keep the histogram
// usable rather than aborting the process over a misconfiguration.
LOG(ERROR) << "A histogram needs at least one bound, falling back to 1";
_bounds.push_back(1.0);
}
}
std::ostream& operator<<(std::ostream& os, const Histogram::Value& v) {
os << "{\"count\":" << v.num << ",\"sum\":"
<< json_double_to_string(v.sum)
<< ",\"counts\":[";
size_t nbuckets = std::min(v.num_buckets, MAX_HISTOGRAM_BUCKETS);
for (size_t i = 0; i < nbuckets; ++i) {
if (i != 0) {
os << ',';
}
os << v.counts[i];
}
return os << "]}";
}
Histogram::Histogram(const BucketSchema& schema)
: _schema(schema)
// Both identities carry `num_buckets` so that a value combined out of no
// agent at all still knows how wide it is.
, _combiner(std::make_shared<combiner_type>(value_type(schema.num_buckets()),
value_type(schema.num_buckets())))
, _sampler(nullptr) {
}
Histogram::Histogram(const butil::StringPiece& name, const BucketSchema& schema)
: Histogram(schema) {
expose(name);
}
Histogram::Histogram(const butil::StringPiece& prefix,
const butil::StringPiece& name,
const BucketSchema& schema)
: Histogram(schema) {
expose_as(prefix, name);
}
Histogram::~Histogram() {
// Calling hide() manually is a MUST required by Variable.
hide();
if (_sampler != nullptr) {
_sampler->destroy();
}
}
Histogram& Histogram::operator<<(double value) {
if (BAIDU_UNLIKELY(!butil::IsFinite(value))) {
LOG_EVERY_SECOND(WARNING) << "Ignoring non-finite value=" << value
<< " recorded into Histogram(" << name() << ')';
return *this;
}
agent_type* agent = _combiner->get_or_create_tls_agent();
if (BAIDU_UNLIKELY(agent == nullptr)) {
LOG(FATAL) << "Fail to create agent";
return *this;
}
// `_schema` outlives the call, the op only borrows it to find the bucket.
agent->element.modify(detail::AddSampleToHistogram(&_schema), value);
return *this;
}
Histogram::sampler_type* Histogram::get_sampler() {
if (_sampler == nullptr) {
_sampler = new sampler_type(this);
_sampler->set_debug_name(name());
_sampler->schedule();
}
return _sampler;
}
int Histogram::expose_impl(const butil::StringPiece& prefix,
const butil::StringPiece& name,
DisplayFilter display_filter) {
int rc = Variable::expose_impl(prefix, name, display_filter);
if (rc == 0 && _sampler != nullptr) {
_sampler->set_debug_name(this->name());
}
return rc;
}
void Histogram::describe(std::ostream& os, bool /*quote_string*/) const {
value_type v = get_value();
os << "{\"count\":" << v.num
<< ",\"sum\":" << json_double_to_string(v.sum)
<< ",\"bounds\":[";
for (size_t i = 0; i < _schema.num_bounds(); ++i) {
if (i != 0) {
os << ',';
}
// validate_bounds() already dropped the non finite ones, so this only
// keeps the whole json on one spelling rule.
os << json_double_to_string(_schema.bound_at(i));
}
os << "],\"counts\":[";
for (size_t i = 0; i < _schema.num_buckets(); ++i) {
if (i != 0) {
os << ',';
}
os << v.counts[i];
}
os << "]}";
}
const std::vector<MetricFamily>& Histogram::list_metric_families() {
// Deliberately leaked. A function local static registers its destructor
// with atexit on the first call, which is whenever the first Histogram is
// dumped or exposed, and that can be later than the construction of a
// static object which reads this from its own destructor.
static auto families = new std::vector<MetricFamily>{
{"", "histogram", {"le"}},
};
return *families;
}
bool Histogram::dump(Dumper* dumper, const DumpOptions&,
const std::string& name) const {
// One comment for the whole family, then one metric per bucket.
// Going through Dumper::dump() instead would make the prometheus
// dumper prepend a "# TYPE ... gauge" to every single bucket.
if (!dumper->dump_comment(name, list_metric_families()[0].type)) {
return false;
}
return dump_samples(dumper, 0, name, butil::StringPiece());
}
// Builds the name of one sample into `key': `name' + `suffix', then the brace
// group holding `labels' (the labels of the enclosing MultiDimension, if any)
// followed by `le' when the sample is a bucket. Namely
// foo_bucket{method="echo",le="10"} or foo_sum{method="echo"}
// A sample with neither gets no braces at all: `foo_sum'.
static void make_sample_key(std::string* key, const std::string& name,
const char* suffix,
butil::StringPiece labels,
butil::StringPiece le) {
key->assign(name);
key->append(suffix);
if (labels.empty() && le.empty()) {
return;
}
key->push_back('{');
key->append(labels.data(), labels.size());
if (!le.empty()) {
if (!labels.empty()) {
key->push_back(',');
}
key->append("le=\"");
key->append(le.data(), le.size());
key->push_back('"');
}
key->push_back('}');
}
bool Histogram::dump_samples(Dumper* dumper, size_t /*family_index*/,
const std::string& name,
butil::StringPiece labels) const {
value_type v = get_value();
std::string key;
std::string bound;
// Prometheus wants the buckets cumulative: foo_bucket{le="20"} counts
// everything not greater than 20, not just what falls between 10 and 20.
// The last one therefore holds every recorded value, which is exactly the
// `le="+Inf" must equal _count` rule of the format.
uint64_t cumulative = 0;
for (size_t i = 0; i < _schema.num_buckets(); ++i) {
cumulative += v.counts[i];
if (_schema.is_inf_bucket(i)) {
bound = "+Inf";
} else {
bound = detail::prometheus_double_to_string(_schema.bound_at(i));
}
make_sample_key(&key, name, "_bucket", labels, bound);
if (!dumper->dump_mvar(key, butil::Uint64ToString(cumulative))) {
return false;
}
}
make_sample_key(&key, name, "_sum", labels, butil::StringPiece());
if (!dumper->dump_mvar(key, detail::prometheus_double_to_string(v.sum))) {
return false;
}
make_sample_key(&key, name, "_count", labels, butil::StringPiece());
return dumper->dump_mvar(key, butil::Int64ToString(v.num));
}
} // namespace bvar