| // 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 |