blob: d262384196dc5f8dbefef9fd8f37cb97eede189f [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.
#ifndef BVAR_HISTOGRAM_H
#define BVAR_HISTOGRAM_H
#include <stdint.h> // int64_t, uint64_t
#include <algorithm> // std::lower_bound
#include <initializer_list> // std::initializer_list
#include <string> // std::string
#include <vector> // std::vector
#include "butil/strings/string_piece.h" // butil::StringPiece
#include "bvar/variable.h" // Variable
#include "bvar/detail/combiner.h" // AgentCombiner
#include "bvar/detail/sampler.h" // ReducerSampler
#include "bvar/detail/series.h" // HasPlottableSeries
namespace bvar {
// Maximum number of buckets of a Histogram, the implicit +Inf one included.
// Namely a Histogram::BucketSchema takes at most MAX_HISTOGRAM_BUCKETS - 1
// bounds.
static const size_t MAX_HISTOGRAM_BUCKETS = 32;
// Bucketed distribution of the recorded values.
//
// Each bucket count accumulates since construction and never decreases. The
// counts are made cumulative across buckets when exported, which is what
// prometheus expects of a histogram and what makes rate() work:
//
// bvar::Histogram g_lat("foo_latency", {10, 50, 100, 500, 1000, 5000});
// ...
// g_lat << latency_us;
//
// Choose the bounds for the values being recorded. The caller supplies them
// as an initializer_list or generates a vector for a custom bucketing policy:
//
// bvar::Histogram g_size("foo_size", {128, 1024, 8192, 65536});
//
// To read a recent distribution in process, wrap it in a Window and read the
// bucket counts off its value:
//
// bvar::Window<bvar::Histogram> g_lat_1m(&g_lat, 60);
// ...
// bvar::Histogram::Value v = g_lat_1m.get_value();
//
// Unlike Percentile, the op of a Histogram has an inverse, so the Window above
// is computed by subtracting two samples rather than by resetting the Histogram,
// and get_value() keeps returning the whole history meanwhile.
//
// Do NOT expose such a Window for prometheus to scrape: its value is a
// distribution, which describe() can only write as json and the prometheus
// service therefore skips. Expose the Histogram itself and let the monitoring
// system window it with rate(foo_latency_bucket[1m]).
class Histogram : public Variable {
public:
// Immutable and ascending upper bounds of the buckets of a Histogram.
// The semantics follow the prometheus `le` label: bucket i counts values v
// satisfying bound_at(i-1) < v <= bound_at(i), followed by a +Inf bucket.
class BucketSchema {
public:
// `bounds` must be finite, non-empty, strictly ascending, and hold no
// more than MAX_HISTOGRAM_BUCKETS - 1 elements. Invalid bounds are
// logged and dropped rather than aborting the process.
BucketSchema(std::initializer_list<double> bounds);
explicit BucketSchema(const std::vector<double>& bounds);
size_t index_of(double value) const {
return std::lower_bound(_bounds.begin(), _bounds.end(), value) -
_bounds.begin();
}
size_t num_buckets() const { return _bounds.size() + 1; }
size_t num_bounds() const { return _bounds.size(); }
double bound_at(size_t index) const { return _bounds[index]; }
const std::vector<double>& bounds() const { return _bounds; }
bool is_inf_bucket(size_t index) const {
return index == _bounds.size();
}
private:
void validate_bounds();
std::vector<double> _bounds;
};
// How many values fell into each bucket, plus their sum and total count.
struct Value {
Value() : Value(0) {}
explicit Value(size_t nbuckets)
: counts{}, sum(0), num(0), num_buckets(nbuckets) {}
void add(size_t bucket_index, double value) {
++counts[bucket_index];
sum += value;
++num;
}
void operator+=(const Value& rhs) {
for (size_t i = 0; i < MAX_HISTOGRAM_BUCKETS; ++i) {
counts[i] += rhs.counts[i];
}
sum += rhs.sum;
num += rhs.num;
num_buckets = std::max(num_buckets, rhs.num_buckets);
}
void operator-=(const Value& rhs) {
for (size_t i = 0; i < MAX_HISTOGRAM_BUCKETS; ++i) {
counts[i] -= rhs.counts[i];
}
sum -= rhs.sum;
num -= rhs.num;
num_buckets = std::max(num_buckets, rhs.num_buckets);
}
double get_average_double() const {
return num == 0 ? 0.0 : sum / (double)num;
}
uint64_t counts[MAX_HISTOGRAM_BUCKETS];
double sum;
int64_t num;
size_t num_buckets;
};
struct Op {
void operator()(Value& lhs, const Value& rhs) const { lhs += rhs; }
};
struct InvOp {
void operator()(Value& lhs, const Value& rhs) const { lhs -= rhs; }
};
typedef Value value_type;
typedef detail::ReducerSampler<Histogram, value_type, Op, InvOp> sampler_type;
typedef detail::AgentCombiner<value_type, value_type, Op> combiner_type;
typedef combiner_type::self_shared_type shared_combiner_type;
typedef combiner_type::Agent agent_type;
explicit Histogram(const BucketSchema& schema);
Histogram(const butil::StringPiece& name, const BucketSchema& schema);
Histogram(const butil::StringPiece& prefix, const butil::StringPiece& name,
const BucketSchema& schema);
~Histogram() override;
Histogram& operator<<(double value);
// Number and approximate floating-point sum of the values recorded so far.
int64_t count() const { return get_value().num; }
double sum() const { return get_value().sum; }
double average() const { return get_value().get_average_double(); }
const BucketSchema& schema() const { return _schema; }
bool valid() const {
return _combiner != nullptr && _combiner->valid();
}
void describe(std::ostream& os, bool quote_string) const override;
// Emits the prometheus histogram family.
bool dump(Dumper* dumper, const DumpOptions& options,
const std::string& name) const override;
// The composite metric contract, see bvar/variable.h
// `_bucket`, `_sum` and `_count` are the series of one family rather
// than three families of their own, hence the single entry with an
// empty suffix.
static const std::vector<MetricFamily>& list_metric_families();
bool dump_samples(Dumper* dumper, size_t family_index,
const std::string& name,
butil::StringPiece labels) const;
// The contract of Window<>/ReducerSampler
Op op() const { return Op(); }
InvOp inv_op() const { return InvOp(); }
// Expose the shared data carrier, so that ReducerSampler holds it instead
// of `this`. Sampling then keeps reading valid memory even if this
// Percentile is destructed before the sampler is recycled.
shared_combiner_type share_combiner() const { return _combiner; }
sampler_type* get_sampler();
private:
int expose_impl(const butil::StringPiece& prefix,
const butil::StringPiece& name,
DisplayFilter display_filter) override;
// Snapshot of all the values recorded so far. Walks through every thread
// that ever recorded into this Histogram.
value_type get_value() const { return _combiner->combine_agents(); }
BucketSchema _schema;
shared_combiner_type _combiner;
sampler_type* _sampler;
};
std::ostream& operator<<(std::ostream& os, const Histogram::Value& value);
namespace detail {
// A distribution has no plot to draw, same reason as the one spelled out in
// Histogram::expose_impl(). Without this an exposed Window<Histogram> would
// keep 174 samples of a value this wide and feed flot.js json objects.
template <>
struct HasPlottableSeries<Histogram::Value> : butil::false_type {};
// The op of the writing path takes a recorded value rather than another
// Histogram::Value, and needs the schema to find its bucket.
struct AddSampleToHistogram {
explicit AddSampleToHistogram(const Histogram::BucketSchema* s)
: schema(s) {}
void operator()(Histogram::Value& lhs, double value) const {
lhs.add(schema->index_of(value), value);
}
const Histogram::BucketSchema* schema;
};
} // namespace detail
} // namespace bvar
#endif // BVAR_HISTOGRAM_H