Add prometheus metrics for Central controllers (#1969)

* add header-only prometheus lib to ext

* rename folder

* Undo rename directory

* prometheus simpleapi included on mac & linux

* wip

* wire up some controller stats

* Get windows building with prometheus

* bsd build flags for prometheus

* Fix multiple network join from environment entrypoint.sh.release (#1961)

* _bond_m guards _bond, not _paths_m (#1965)

* Fix: warning: mutex '_aqm_m' is not held on every path through here [-Wthread-safety-analysis] (#1964)

* Serve prom metrics from /metrics endpoint

* Add prom metrics for Central controller specific things

* reorganize metric initialization

* testing out a labled gauge on Networks

* increment error counter on throw

* Consolidate metrics definitions

Put all metric definitions into node/Metrics.hpp.  Accessed as needed
from there.

* Revert "testing out a labled gauge on Networks"

This reverts commit 499ed6d95e11452019cdf48e32ed4cd878c2705b.

* still blows up but adding to the record for completeness right now

* Fix runtime issues with metrics

* Add metrics files to visual studio project

* Missed an "extern"

* add copyright headers to new files

* Add metrics for sent/received bytes (total)

* put /metrics endpoint behind auth

* sendto returns int on Win32

---------

Co-authored-by: Leonardo Amaral <leleobhz@users.noreply.github.com>
Co-authored-by: Brenton Bostick <bostick@gmail.com>
This commit is contained in:
Grant Limberg 2023-04-21 12:12:43 -07:00 committed by GitHub
parent 0b03ad9a21
commit 8e6e4ede6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 4023 additions and 25 deletions

View file

@ -0,0 +1,40 @@
#pragma once
#include <type_traits>
#include <atomic>
namespace prometheus {
template <typename FloatingType>
inline std::atomic<FloatingType>& atomic_add_for_floating_types(std::atomic<FloatingType>& value,
const FloatingType& add) {
FloatingType desired;
FloatingType expected = value.load(std::memory_order_relaxed);
do {
desired = expected + add;
} while (!value.compare_exchange_weak(expected, desired));
return value;
}
template <typename FloatingType, class = typename std::enable_if<std::is_floating_point<FloatingType>::value, int>::type>
inline std::atomic<FloatingType>& operator++(std::atomic<FloatingType>& value) {
return atomic_add_for_floating_types(value, 1.0);
}
template <typename FloatingType, class = typename std::enable_if<std::is_floating_point<FloatingType>::value, int>::type>
inline std::atomic<FloatingType>& operator+=(std::atomic<FloatingType>& value, const FloatingType& val) {
return atomic_add_for_floating_types(value, val);
}
template <typename FloatingType, class = typename std::enable_if<std::is_floating_point<FloatingType>::value, int>::type>
inline std::atomic<FloatingType>& operator--(std::atomic<FloatingType>& value) {
return atomic_add_for_floating_types(value, -1.0);
}
template <typename FloatingType, class = typename std::enable_if<std::is_floating_point<FloatingType>::value, int>::type>
inline std::atomic<FloatingType>& operator-=(std::atomic<FloatingType>& value, const FloatingType& val) {
return atomic_add_for_floating_types(value, -val);
}
}

View file

@ -0,0 +1,72 @@
#pragma once
#include "prometheus/metric.h"
#include "prometheus/family.h"
#include <chrono>
namespace prometheus {
class Benchmark : public Metric {
#ifndef NDEBUG
bool already_started = false;
#endif
std::chrono::time_point<std::chrono::high_resolution_clock> start_;
std::chrono::time_point<std::chrono::high_resolution_clock>::duration elapsed = std::chrono::time_point<std::chrono::high_resolution_clock>::duration::zero(); // elapsed time
public:
using Value = double;
using Family = CustomFamily<Benchmark>;
static const Metric::Type static_type = Metric::Type::Counter;
Benchmark() : Metric(Metric::Type::Counter) {}
void start() {
#ifndef NDEBUG
if (already_started)
throw std::runtime_error("try to start already started counter");
else
already_started = true;
#endif
start_ = std::chrono::high_resolution_clock::now();
}
void stop() {
#ifndef NDEBUG
if (already_started == false)
throw std::runtime_error("try to stop already stoped counter");
#endif
std::chrono::time_point<std::chrono::high_resolution_clock> stop;
stop = std::chrono::high_resolution_clock::now();
elapsed += stop - start_;
#ifndef NDEBUG
already_started = false;
#endif
}
double Get() const {
return std::chrono::duration_cast<std::chrono::duration<double>>(elapsed).count();
}
virtual ClientMetric Collect() const {
ClientMetric metric;
metric.counter.value = Get();
return metric;
}
};
} // namespace prometheus

View file

@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <map>
#include "registry.h"
namespace prometheus {
template <typename CustomMetric>
class Builder {
Family::Labels labels_;
std::string name_;
std::string help_;
public:
Builder& Labels(const std::map<const std::string, const std::string>& labels) {
labels_ = labels;
return *this;
}
Builder& Name(const std::string& name) {
name_ = name;
return *this;
}
Builder& Help(const std::string& help) {
help_ = help;
return *this;
}
CustomFamily<CustomMetric>& Register(Registry& registry) {
return registry.Add<CustomFamily<CustomMetric>>(name_, help_, labels_);
}
};
}

View file

@ -0,0 +1,194 @@
#pragma once
#include <array>
#include <cstddef>
#include <functional>
#include <vector>
namespace prometheus {
namespace detail {
class CKMSQuantiles {
public:
struct Quantile {
double quantile;
double error;
double u;
double v;
Quantile(double quantile, double error)
: quantile(quantile),
error(error),
u(2.0 * error / (1.0 - quantile)),
v(2.0 * error / quantile) {}
};
private:
struct Item {
double value;
int g;
int delta;
Item(double value, int lower_delta, int delta)
: value(value), g(lower_delta), delta(delta) {}
};
public:
explicit CKMSQuantiles(const std::vector<Quantile>& quantiles)
: quantiles_(quantiles), count_(0), buffer_{}, buffer_count_(0) {}
void insert(double value) {
buffer_[buffer_count_] = value;
++buffer_count_;
if (buffer_count_ == buffer_.size()) {
insertBatch();
compress();
}
}
double get(double q) {
insertBatch();
compress();
if (sample_.empty()) {
return std::numeric_limits<double>::quiet_NaN();
}
int rankMin = 0;
const auto desired = static_cast<int>(q * static_cast<double>(count_));
const auto bound = desired + (allowableError(desired) / 2);
auto it = sample_.begin();
decltype(it) prev;
auto cur = it++;
while (it != sample_.end()) {
prev = cur;
cur = it++;
rankMin += prev->g;
if (rankMin + cur->g + cur->delta > bound) {
return prev->value;
}
}
return sample_.back().value;
}
void reset() {
count_ = 0;
sample_.clear();
buffer_count_ = 0;
}
private:
double allowableError(int rank) {
auto size = sample_.size();
double minError = static_cast<double>(size + 1);
for (const auto& q : quantiles_.get()) {
double error;
if (static_cast<double>(rank) <= q.quantile * static_cast<double>(size)) {
error = q.u * static_cast<double>(size - rank);
}
else {
error = q.v * rank;
}
if (error < minError) {
minError = error;
}
}
return minError;
}
bool insertBatch() {
if (buffer_count_ == 0) {
return false;
}
std::sort(buffer_.begin(), buffer_.begin() + buffer_count_);
std::size_t start = 0;
if (sample_.empty()) {
sample_.emplace_back(buffer_[0], 1, 0);
++start;
++count_;
}
std::size_t idx = 0;
std::size_t item = idx++;
for (std::size_t i = start; i < buffer_count_; ++i) {
double v = buffer_[i];
while (idx < sample_.size() && sample_[item].value < v) {
item = idx++;
}
if (sample_[item].value > v) {
--idx;
}
int delta;
if (idx - 1 == 0 || idx + 1 == sample_.size()) {
delta = 0;
}
else {
delta = static_cast<int>(std::floor(allowableError(static_cast<int>(idx + 1)))) + 1;
}
sample_.emplace(sample_.begin() + idx, v, 1, delta);
count_++;
item = idx++;
}
buffer_count_ = 0;
return true;
}
void compress() {
if (sample_.size() < 2) {
return;
}
std::size_t idx = 0;
std::size_t prev;
std::size_t next = idx++;
while (idx < sample_.size()) {
prev = next;
next = idx++;
if (sample_[prev].g + sample_[next].g + sample_[next].delta <=
allowableError(static_cast<int>(idx - 1))) {
sample_[next].g += sample_[prev].g;
sample_.erase(sample_.begin() + prev);
}
}
}
private:
const std::reference_wrapper<const std::vector<Quantile>> quantiles_;
std::size_t count_;
std::vector<Item> sample_;
std::array<double, 500> buffer_;
std::size_t buffer_count_;
};
} // namespace detail
} // namespace prometheus

View file

@ -0,0 +1,94 @@
#pragma once
#include <cstdint>
#include <string>
#include <tuple>
#include <vector>
namespace prometheus {
// ñòðóêòóðà, â êîòîðóþ êîïèðóþòñÿ çíà÷åíèÿ ìåòðèê ïåðåä èõ ñåðèàëèçàöèåé
struct ClientMetric {
// Label
struct Label {
std::string name;
std::string value;
Label(const std::string name_, const std::string value_) : name(name_), value(value_) {}
bool operator<(const Label& rhs) const {
return std::tie(name, value) < std::tie(rhs.name, rhs.value);
}
bool operator==(const Label& rhs) const {
return std::tie(name, value) == std::tie(rhs.name, rhs.value);
}
};
std::vector<Label> label;
// Counter
struct Counter {
double value = 0.0;
};
Counter counter;
// Gauge
struct Gauge {
double value = 0.0;
};
Gauge gauge;
// Summary
struct Quantile {
double quantile = 0.0;
double value = 0.0;
};
struct Summary {
std::uint64_t sample_count = 0;
double sample_sum = 0.0;
std::vector<Quantile> quantile;
};
Summary summary;
// Histogram
struct Bucket {
std::uint64_t cumulative_count = 0;
double upper_bound = 0.0;
};
struct Histogram {
std::uint64_t sample_count = 0;
double sample_sum = 0.0;
std::vector<Bucket> bucket;
};
Histogram histogram;
// Untyped
struct Untyped {
double value = 0;
};
Untyped untyped;
// Timestamp
std::int64_t timestamp_ms = 0;
};
} // namespace prometheus

View file

@ -0,0 +1,27 @@
#pragma once
#include <vector>
#include "prometheus/metric_family.h"
namespace prometheus {
/// @brief Interface implemented by anything that can be used by Prometheus to
/// collect metrics.
///
/// A Collectable has to be registered for collection. See Registry.
class Collectable {
public:
//Collectable() = default;
virtual ~Collectable() = default;
using MetricFamilies = std::vector<MetricFamily>;
/// \brief Returns a list of metrics and their samples.
virtual MetricFamilies Collect() const = 0;
};
} // namespace prometheus

View file

@ -0,0 +1,112 @@
#pragma once
#include "prometheus/atomic_floating.h"
#include "prometheus/metric.h"
#include "prometheus/family.h"
#include "prometheus/builder.h"
#include <atomic>
namespace prometheus {
/// \brief A counter metric to represent a monotonically increasing value.
///
/// This class represents the metric type counter:
/// https://prometheus.io/docs/concepts/metric_types/#counter
///
/// The value of the counter can only increase. Example of counters are:
/// - the number of requests served
/// - tasks completed
/// - errors
///
/// Do not use a counter to expose a value that can decrease - instead use a
/// Gauge.
///
/// The class is thread-safe. No concurrent call to any API of this type causes
/// a data race.
template <typename Value_ = uint64_t>
class Counter : public Metric {
std::atomic<Value_> value{ 0 };
public:
using Value = Value_;
using Family = CustomFamily<Counter<Value>>;
static const Metric::Type static_type = Metric::Type::Counter;
Counter() : Metric (Metric::Type::Counter) {} ///< \brief Create a counter that starts at 0.
// original API
void Increment() { ///< \brief Increment the counter by 1.
++value;
}
void Increment(const Value& val) { ///< \brief Increment the counter by a given amount. The counter will not change if the given amount is negative.
if (val > 0)
value += val;
}
const Value Get() const { ///< \brief Get the current value of the counter.
return value;
}
virtual ClientMetric Collect() const { ///< /// \brief Get the current value of the counter. Collect is called by the Registry when collecting metrics.
ClientMetric metric;
metric.counter.value = static_cast<double>(value);
return metric;
}
// new API
Counter& operator ++() {
++value;
return *this;
}
Counter& operator++ (int) {
++value;
return *this;
}
Counter& operator += (const Value& val) {
value += val;
return *this;
}
};
/// \brief Return a builder to configure and register a Counter metric.
///
/// @copydetails Family<>::Family()
///
/// Example usage:
///
/// \code
/// auto registry = std::make_shared<Registry>();
/// auto& counter_family = prometheus::BuildCounter()
/// .Name("some_name")
/// .Help("Additional description.")
/// .Labels({{"key", "value"}})
/// .Register(*registry);
///
/// ...
/// \endcode
///
/// \return An object of unspecified type T, i.e., an implementation detail
/// except that it has the following members:
///
/// - Name(const std::string&) to set the metric name,
/// - Help(const std::string&) to set an additional description.
/// - Label(const std::map<std::string, std::string>&) to assign a set of
/// key-value pairs (= labels) to the metric.
///
/// To finish the configuration of the Counter metric, register it with
/// Register(Registry&).
using BuildCounter = Builder<Counter<double>>;
} // namespace prometheus

View file

@ -0,0 +1,355 @@
#pragma once
#include <cstddef>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include <cassert>
#include "prometheus/collectable.h"
#include "prometheus/metric.h"
#include "prometheus/hash.h"
namespace prometheus {
/// \brief A metric of type T with a set of labeled dimensions.
///
/// One of Prometheus main feature is a multi-dimensional data model with time
/// series data identified by metric name and key/value pairs, also known as
/// labels. A time series is a series of data points indexed (or listed or
/// graphed) in time order (https://en.wikipedia.org/wiki/Time_series).
///
/// An instance of this class is exposed as multiple time series during
/// scrape, i.e., one time series for each set of labels provided to Add().
///
/// For example it is possible to collect data for a metric
/// `http_requests_total`, with two time series:
///
/// - all HTTP requests that used the method POST
/// - all HTTP requests that used the method GET
///
/// The metric name specifies the general feature of a system that is
/// measured, e.g., `http_requests_total`. Labels enable Prometheus's
/// dimensional data model: any given combination of labels for the same
/// metric name identifies a particular dimensional instantiation of that
/// metric. For example a label for 'all HTTP requests that used the method
/// POST' can be assigned with `method= "POST"`.
///
/// Given a metric name and a set of labels, time series are frequently
/// identified using this notation:
///
/// <metric name> { < label name >= <label value>, ... }
///
/// It is required to follow the syntax of metric names and labels given by:
/// https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
///
/// The following metric and label conventions are not required for using
/// Prometheus, but can serve as both a style-guide and a collection of best
/// practices: https://prometheus.io/docs/practices/naming/
///
/// tparam T One of the metric types Counter, Gauge, Histogram or Summary.
class Family : public Collectable {
public:
using Hash = std::size_t;
using Label = std::pair<const std::string, const std::string>;
using Labels = std::map <const std::string, const std::string>;
using MetricPtr = std::unique_ptr<Metric>;
const Metric::Type type;
const std::string name;
const std::string help;
const Labels constant_labels;
mutable std::mutex mutex;
std::unordered_map<Hash, MetricPtr> metrics;
std::unordered_map<Hash, Labels> labels;
std::unordered_map<Metric*, Hash> labels_reverse_lookup;
/// \brief Compute the hash value of a map of labels.
///
/// \param labels The map that will be computed the hash value.
///
/// \returns The hash value of the given labels.
static Hash hash_labels (const Labels& labels) {
size_t seed = 0;
for (const Label& label : labels)
detail::hash_combine (&seed, label.first, label.second);
return seed;
}
static bool isLocaleIndependentDigit (char c) { return '0' <= c && c <= '9'; }
static bool isLocaleIndependentAlphaNumeric (char c) { return isLocaleIndependentDigit(c) || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'); }
bool nameStartsValid (const std::string& name) {
if (name.empty()) return false; // must not be empty
if (isLocaleIndependentDigit(name.front())) return false; // must not start with a digit
if (name.compare(0, 2, "__") == 0) return false; // must not start with "__"
return true;
}
/// \brief Check if the metric name is valid
///
/// The metric name regex is "[a-zA-Z_:][a-zA-Z0-9_:]*"
///
/// \see https://prometheus.io/docs/concepts/data_model/
///
/// \param name metric name
/// \return true is valid, false otherwise
bool CheckMetricName (const std::string& name) {
if (!nameStartsValid(name))
return false;
for (const char& c : name)
if ( !isLocaleIndependentAlphaNumeric(c) && c != '_' && c != ':' )
return false;
return true;
}
/// \brief Check if the label name is valid
///
/// The label name regex is "[a-zA-Z_][a-zA-Z0-9_]*"
///
/// \see https://prometheus.io/docs/concepts/data_model/
///
/// \param name label name
/// \return true is valid, false otherwise
bool CheckLabelName (const std::string& name) {
if (!nameStartsValid(name))
return false;
for (const char& c : name)
if (!isLocaleIndependentAlphaNumeric(c) && c != '_')
return false;
return true;
}
/// \brief Create a new metric.
///
/// Every metric is uniquely identified by its name and a set of key-value
/// pairs, also known as labels. Prometheus's query language allows filtering
/// and aggregation based on metric name and these labels.
///
/// This example selects all time series that have the `http_requests_total`
/// metric name:
///
/// http_requests_total
///
/// It is possible to assign labels to the metric name. These labels are
/// propagated to each dimensional data added with Add(). For example if a
/// label `job= "prometheus"` is provided to this constructor, it is possible
/// to filter this time series with Prometheus's query language by appending
/// a set of labels to match in curly braces ({})
///
/// http_requests_total{job= "prometheus"}
///
/// For further information see: [Quering Basics]
/// (https://prometheus.io/docs/prometheus/latest/querying/basics/)
///
/// \param name Set the metric name.
/// \param help Set an additional description.
/// \param constant_labels Assign a set of key-value pairs (= labels) to the
/// metric. All these labels are propagated to each time series within the
/// metric.
/// \throw std::runtime_exception on invalid metric or label names.
Family (Metric::Type type_, const std::string& name_, const std::string& help_, const Labels& constant_labels_)
: type(type_), name(name_), help(help_), constant_labels(constant_labels_) {
if (!CheckMetricName(name_))
throw std::invalid_argument("Invalid metric name");
for (const Label& label_pair : constant_labels) {
const std::string& label_name = label_pair.first;
if (!CheckLabelName(label_name))
throw std::invalid_argument("Invalid label name");
}
}
/// \brief Remove the given dimensional data.
///
/// \param metric Dimensional data to be removed. The function does nothing,
/// if the given metric was not returned by Add().
void Remove (Metric* metric) {
std::lock_guard<std::mutex> lock{ mutex };
if (labels_reverse_lookup.count(metric) == 0)
return;
const Hash hash = labels_reverse_lookup.at(metric);
metrics.erase(hash);
labels.erase(hash);
labels_reverse_lookup.erase(metric);
}
/// \brief Returns true if the dimensional data with the given labels exist
///
/// \param labels A set of key-value pairs (= labels) of the dimensional data.
bool Has (const Labels& labels) const {
const Hash hash = hash_labels (labels);
std::lock_guard<std::mutex> lock{ mutex };
return metrics.find(hash) != metrics.end();
}
/// \brief Returns the name for this family.
///
/// \return The family name.
const std::string& GetName() const {
return name;
}
/// \brief Returns the constant labels for this family.
///
/// \return All constant labels as key-value pairs.
const Labels& GetConstantLabels() const {
return constant_labels;
}
/// \brief Returns the current value of each dimensional data.
///
/// Collect is called by the Registry when collecting metrics.
///
/// \return Zero or more samples for each dimensional data.
MetricFamilies Collect() const override {
std::lock_guard<std::mutex> lock{ mutex };
if (metrics.empty())
return {};
MetricFamily family = MetricFamily{};
family.type = type;
family.name = name;
family.help = help;
family.metric.reserve(metrics.size());
for (const std::pair<const Hash, MetricPtr>& metric_pair : metrics) {
ClientMetric collected = metric_pair.second->Collect();
for (const Label& constant_label : constant_labels)
collected.label.emplace_back(ClientMetric::Label(constant_label.first, constant_label.second));
const Labels& metric_labels = labels.at(metric_pair.first);
for (const Label& metric_label : metric_labels)
collected.label.emplace_back(ClientMetric::Label(metric_label.first, metric_label.second));
family.metric.push_back(std::move(collected));
}
return { family };
}
};
template <typename CustomMetric>
class CustomFamily : public Family {
public:
static const Metric::Type static_type = CustomMetric::static_type;
CustomFamily(const std::string& name, const std::string& help, const Family::Labels& constant_labels)
: Family(static_type, name, help, constant_labels) {}
/// \brief Add a new dimensional data.
///
/// Each new set of labels adds a new dimensional data and is exposed in
/// Prometheus as a time series. It is possible to filter the time series
/// with Prometheus's query language by appending a set of labels to match in
/// curly braces ({})
///
/// http_requests_total{job= "prometheus",method= "POST"}
///
/// \param labels Assign a set of key-value pairs (= labels) to the
/// dimensional data. The function does nothing, if the same set of labels
/// already exists.
/// \param args Arguments are passed to the constructor of metric type T. See
/// Counter, Gauge, Histogram or Summary for required constructor arguments.
/// \return Return the newly created dimensional data or - if a same set of
/// labels already exists - the already existing dimensional data.
/// \throw std::runtime_exception on invalid label names.
template <typename... Args>
CustomMetric& Add (const Labels& new_labels, Args&&... args) {
const Hash hash = hash_labels (new_labels);
std::lock_guard<std::mutex> lock{ mutex };
// try to find existing one
auto metrics_iter = metrics.find(hash);
if (metrics_iter != metrics.end()) {
#ifndef NDEBUG
// check that we have stored labels for this existing metric
auto labels_iter = labels.find(hash);
assert(labels_iter != labels.end());
const Labels& stored_labels = labels_iter->second;
assert(new_labels == stored_labels);
#endif
return dynamic_cast<CustomMetric&>(*metrics_iter->second);
}
// check labels before create the new one
for (const Label& label_pair : new_labels) {
const std::string& label_name = label_pair.first;
if (!CheckLabelName(label_name))
throw std::invalid_argument("Invalid label name");
if (constant_labels.count(label_name))
throw std::invalid_argument("Label name already present in constant labels");
}
// create new one
std::unique_ptr<CustomMetric> metric_ptr (new CustomMetric(std::forward<Args>(args)...));
CustomMetric& metric = *metric_ptr;
const auto stored_metric = metrics.insert(std::make_pair(hash, std::move(metric_ptr)));
assert(stored_metric.second);
labels.insert({ hash, new_labels });
labels_reverse_lookup.insert({ stored_metric.first->second.get(), hash });
return metric;
}
/// \brief Return a builder to configure and register a Counter metric.
///
/// @copydetails family_base_t<>::family_base_t()
///
/// Example usage:
///
/// \code
/// auto registry = std::make_shared<Registry>();
/// auto& counter_family = prometheus::Counter_family::build("some_name", "Additional description.", {{"key", "value"}}, *registry);
///
/// ...
/// \endcode
///
/// \return An object of unspecified type T, i.e., an implementation detail
/// except that it has the following members:
///
/// - Name(const std::string&) to set the metric name,
/// - Help(const std::string&) to set an additional description.
/// - Label(const std::map<std::string, std::string>&) to assign a set of
/// key-value pairs (= labels) to the metric.
///
/// To finish the configuration of the Counter metric, register it with
/// Register(Registry&).
template <typename Registry>
static CustomFamily& Build(Registry& registry, const std::string& name, const std::string& help, const Family::Labels& labels = Family::Labels()) {
return registry.template Add<CustomFamily>(name, help, labels);
}
};
} // namespace prometheus

View file

@ -0,0 +1,205 @@
#pragma once
#include "prometheus/collectable.h"
#include "prometheus/text_serializer.h"
#include "prometheus/metric_family.h"
#include <jdl/httpclientlite.h>
#include <memory>
#include <mutex>
#include <string>
#include <sstream>
#include <vector>
#include <map>
#include <future>
#include <algorithm>
#include <utility>
namespace prometheus {
class Gateway {
using CollectableEntry = std::pair<std::weak_ptr<Collectable>, std::string>;
std::string job_uri_;
std::string labels_;
std::mutex mutex_;
std::vector<CollectableEntry> collectables_;
enum class HttpMethod : uint8_t{
Post,
Put,
Delete,
};
public:
using Labels = std::map<std::string, std::string>;
Gateway(const std::string host, const std::string port,
const std::string jobname, const Labels& labels = {})
: job_uri_(host + ':' + port + std::string("/metrics/job/") + jobname)
, labels_{}
{
std::stringstream label_strm;
for (const auto& label : labels) {
label_strm << "/" << label.first << "/" << label.second;
}
labels_ = label_strm.str();
}
void RegisterCollectable(const std::weak_ptr<Collectable>& collectable,
const Labels* labels = nullptr) {
std::stringstream label_strm;
if (labels != nullptr) {
for (const auto& label : *labels) {
label_strm << "/" << label.first << "/" << label.second;
}
}
CleanupStalePointers(collectables_);
collectables_.emplace_back(std::make_pair(collectable, label_strm.str()));
}
static const Labels GetInstanceLabel(const std::string& hostname) {
if (hostname.empty()) {
return Gateway::Labels{};
}
return Gateway::Labels{{"instance", hostname}};
}
// Push metrics to the given pushgateway.
int Push() {
return push(HttpMethod::Post);
}
std::future<int> AsyncPush() {
return async_push(HttpMethod::Post);
}
// PushAdd metrics to the given pushgateway.
int PushAdd() {
return push(HttpMethod::Put);
}
std::future<int> AsyncPushAdd() {
return async_push(HttpMethod::Put);
}
// Delete metrics from the given pushgateway.
int Delete() {
return performHttpRequest(HttpMethod::Delete, job_uri_, {});
}
// Delete metrics from the given pushgateway.
std::future<int> AsyncDelete() {
return std::async(std::launch::async, [&] { return Delete(); });
}
private:
std::string getUri(const CollectableEntry& collectable) const {
return (job_uri_ + labels_ + collectable.second);
}
int performHttpRequest(HttpMethod /*method*/, const std::string& uri_str, const std::string& body) {
std::lock_guard<std::mutex> l(mutex_);
/* Stub function. The implementation will be later, after connecting the
* additional library of HTTP requests. */
jdl::URI uri(uri_str);
jdl::HTTPResponse response = jdl::HTTPClient::request(jdl::HTTPClient::m_post, uri, body);
return std::stoi(response.response);
}
int push(HttpMethod method) {
const auto serializer = TextSerializer{};
for (const auto& wcollectable : collectables_) {
auto collectable = wcollectable.first.lock();
if (!collectable) {
continue;
}
auto metrics = collectable->Collect();
auto uri = getUri(wcollectable);
std::stringstream body;
serializer.Serialize(body, metrics);
std::string body_str = body.str();
auto status_code = performHttpRequest(method, uri, body_str);
if (status_code < 100 || status_code >= 400) {
return status_code;
}
}
return 200;
}
std::future<int> async_push(HttpMethod method) {
const auto serializer = TextSerializer{};
std::vector<std::future<int>> futures;
for (const auto& wcollectable : collectables_) {
auto collectable = wcollectable.first.lock();
if (!collectable) {
continue;
}
auto metrics = collectable->Collect();
auto uri = getUri(wcollectable);
std::stringstream body;
serializer.Serialize(body, metrics);
auto body_ptr = std::make_shared<std::string>(body.str());
futures.emplace_back(std::async(std::launch::async, [method, &uri, &body_ptr, this] {
return performHttpRequest(method, uri, *body_ptr);
}));
}
const auto reduceFutures = [](std::vector<std::future<int>> lfutures) {
auto final_status_code = 200;
for (auto& future : lfutures) {
auto status_code = future.get();
if (status_code < 100 || status_code >= 400) {
final_status_code = status_code;
}
}
return final_status_code;
};
return std::async(std::launch::async, reduceFutures, std::move(futures));
}
static void CleanupStalePointers(std::vector<CollectableEntry>& collectables) {
collectables.erase(std::remove_if(std::begin(collectables), std::end(collectables),
[](const CollectableEntry& candidate) {
return candidate.first.expired();
}),
std::end(collectables));
}
};
} // namespace prometheus

View file

@ -0,0 +1,128 @@
#pragma once
#include "prometheus/atomic_floating.h"
#include "prometheus/metric.h"
#include "prometheus/family.h"
#include "prometheus/builder.h"
#include <atomic>
#include <ctime>
namespace prometheus {
/// \brief A gauge metric to represent a value that can arbitrarily go up and
/// down.
///
/// The class represents the metric type gauge:
/// https://prometheus.io/docs/concepts/metric_types/#gauge
///
/// Gauges are typically used for measured values like temperatures or current
/// memory usage, but also "counts" that can go up and down, like the number of
/// running processes.
///
/// The class is thread-safe. No concurrent call to any API of this type causes
/// a data race.
template <typename Value_ = uint64_t>
class Gauge : public Metric {
std::atomic<Value_> value { 0 };
public:
using Value = Value_;
using Family = CustomFamily<Gauge<Value>>;
static const Metric::Type static_type = Metric::Type::Gauge;
Gauge() : Metric (static_type) {} ///< \brief Create a gauge that starts at 0.
Gauge(const Value value_) : Metric(static_type), value{ value_ } {} ///< \brief Create a gauge that starts at the given amount.
// original API
void Increment() { ++value; } ///< \brief Increment the gauge by 1.
void Increment(const Value& val) { value += val; } ///< \brief Increment the gauge by the given amount.
void Decrement() { --value; } ///< \brief Decrement the gauge by 1.
void Decrement(const Value& val) { value -= val; } ///< \brief Decrement the gauge by the given amount.
void SetToCurrentTime() { ///< \brief Set the gauge to the current unixtime in seconds.
const time_t time = std::time(nullptr);
value = static_cast<Value>(time);
}
void Set(const Value& val) { value = val; } ///< \brief Set the gauge to the given value.
const Value Get() const { return value; } ///< \brief Get the current value of the gauge.
virtual ClientMetric Collect() const { ///< \brief Get the current value of the gauge. Collect is called by the Registry when collecting metrics.
ClientMetric metric;
metric.gauge.value = static_cast<double>(value);
return metric;
}
// new API
Gauge& operator ++() {
++value;
return *this;
}
Gauge& operator++ (int) {
++value;
return *this;
}
Gauge& operator --() {
--value;
return *this;
}
Gauge& operator-- (int) {
--value;
return *this;
}
Gauge& operator+=(const Value& val) {
value += val;
return *this;
}
Gauge& operator-=(const Value& val) {
value -= val;
return *this;
}
};
/// \brief Return a builder to configure and register a Gauge metric.
///
/// @copydetails Family<>::Family()
///
/// Example usage:
///
/// \code
/// auto registry = std::make_shared<Registry>();
/// auto& gauge_family = prometheus::BuildGauge()
/// .Name("some_name")
/// .Help("Additional description.")
/// .Labels({{"key", "value"}})
/// .Register(*registry);
///
/// ...
/// \endcode
///
/// \return An object of unspecified type T, i.e., an implementation detail
/// except that it has the following members:
///
/// - Name(const std::string&) to set the metric name,
/// - Help(const std::string&) to set an additional description.
/// - Label(const std::map<std::string, std::string>&) to assign a set of
/// key-value pairs (= labels) to the metric.
///
/// To finish the configuration of the Gauge metric register it with
/// Register(Registry&).
using BuildGauge = Builder<Gauge<double>>;
} // namespace prometheus

View file

@ -0,0 +1,50 @@
#pragma once
#include <cstddef>
#include <functional>
namespace prometheus {
namespace detail {
/// \brief Combine a hash value with nothing.
/// It's the boundary condition of this serial functions.
///
/// \param seed Not effect.
inline void hash_combine(std::size_t *) {}
/// \brief Combine the given hash value with another obeject.
///
/// \param seed The given hash value. It's a input/output parameter.
/// \param value The object that will be combined with the given hash value.
template <typename T>
inline void hash_combine(std::size_t *seed, const T &value) {
*seed ^= std::hash<T>{}(value) + 0x9e3779b9 + (*seed << 6) + (*seed >> 2);
}
/// \brief Combine the given hash value with another objects. It's a recursion。
///
/// \param seed The give hash value. It's a input/output parameter.
/// \param value The object that will be combined with the given hash value.
/// \param args The objects that will be combined with the given hash value.
template <typename T, typename... Types>
inline void hash_combine(std::size_t *seed, const T &value,
const Types &...args) {
hash_combine(seed, value);
hash_combine(seed, args...);
}
/// \brief Compute a hash value of the given args.
///
/// \param args The arguments that will be computed hash value.
/// \return The hash value of the given args.
template <typename... Types>
inline std::size_t hash_value(const Types &...args) {
std::size_t seed = 0;
hash_combine(&seed, args...);
return seed;
}
} // namespace detail
} // namespace prometheus

View file

@ -0,0 +1,154 @@
#pragma once
#include <vector>
#include <cassert>
#include <algorithm>
#include "prometheus/metric.h"
#include "prometheus/family.h"
#include "prometheus/counter.h"
namespace prometheus {
/// \brief A histogram metric to represent aggregatable distributions of events.
///
/// This class represents the metric type histogram:
/// https://prometheus.io/docs/concepts/metric_types/#histogram
///
/// A histogram tracks the number of observations and the sum of the observed
/// values, allowing to calculate the average of the observed values.
///
/// At its core a histogram has a counter per bucket. The sum of observations
/// also behaves like a counter as long as there are no negative observations.
///
/// See https://prometheus.io/docs/practices/histograms/ for detailed
/// explanations of histogram usage and differences to summaries.
///
/// The class is thread-safe. No concurrent call to any API of this type causes
/// a data race.
template <typename Value_ = uint64_t>
class Histogram : Metric {
using BucketBoundaries = std::vector<Value_>;
const BucketBoundaries bucket_boundaries_;
std::vector<Counter<Value_>> bucket_counts_;
Gauge<Value_> sum_;
public:
using Value = Value_;
using Family = CustomFamily<Histogram<Value>>;
static const Metric::Type static_type = Metric::Type::Histogram;
/// \brief Create a histogram with manually chosen buckets.
///
/// The BucketBoundaries are a list of monotonically increasing values
/// representing the bucket boundaries. Each consecutive pair of values is
/// interpreted as a half-open interval [b_n, b_n+1) which defines one bucket.
///
/// There is no limitation on how the buckets are divided, i.e, equal size,
/// exponential etc..
///
/// The bucket boundaries cannot be changed once the histogram is created.
Histogram (const BucketBoundaries& buckets)
: Metric(static_type), bucket_boundaries_{ buckets }, bucket_counts_{ buckets.size() + 1 }, sum_{} {
assert(std::is_sorted(std::begin(bucket_boundaries_),
std::end(bucket_boundaries_)));
}
/// \brief Observe the given amount.
///
/// The given amount selects the 'observed' bucket. The observed bucket is
/// chosen for which the given amount falls into the half-open interval [b_n,
/// b_n+1). The counter of the observed bucket is incremented. Also the total
/// sum of all observations is incremented.
void Observe(const Value value) {
// TODO: determine bucket list size at which binary search would be faster
const auto bucket_index = static_cast<std::size_t>(std::distance(
bucket_boundaries_.begin(),
std::find_if(
std::begin(bucket_boundaries_), std::end(bucket_boundaries_),
[value](const double boundary) { return boundary >= value; })));
sum_.Increment(value);
bucket_counts_[bucket_index].Increment();
}
/// \brief Observe multiple data points.
///
/// Increments counters given a count for each bucket. (i.e. the caller of
/// this function must have already sorted the values into buckets).
/// Also increments the total sum of all observations by the given value.
void ObserveMultiple(const std::vector<Value>& bucket_increments,
const Value sum_of_values) {
if (bucket_increments.size() != bucket_counts_.size()) {
throw std::length_error(
"The size of bucket_increments was not equal to"
"the number of buckets in the histogram.");
}
sum_.Increment(sum_of_values);
for (std::size_t i{ 0 }; i < bucket_counts_.size(); ++i) {
bucket_counts_[i].Increment(bucket_increments[i]);
}
}
/// \brief Get the current value of the counter.
///
/// Collect is called by the Registry when collecting metrics.
virtual ClientMetric Collect() const {
auto metric = ClientMetric{};
auto cumulative_count = 0ULL;
metric.histogram.bucket.reserve(bucket_counts_.size());
for (std::size_t i{0}; i < bucket_counts_.size(); ++i) {
cumulative_count += static_cast<std::size_t>(bucket_counts_[i].Value());
auto bucket = ClientMetric::Bucket{};
bucket.cumulative_count = cumulative_count;
bucket.upper_bound = (i == bucket_boundaries_.size()
? std::numeric_limits<double>::infinity()
: bucket_boundaries_[i]);
metric.histogram.bucket.push_back(std::move(bucket));
}
metric.histogram.sample_count = cumulative_count;
metric.histogram.sample_sum = sum_.Get();
return metric;
}
};
/// \brief Return a builder to configure and register a Histogram metric.
///
/// @copydetails Family<>::Family()
///
/// Example usage:
///
/// \code
/// auto registry = std::make_shared<Registry>();
/// auto& histogram_family = prometheus::BuildHistogram()
/// .Name("some_name")
/// .Help("Additional description.")
/// .Labels({{"key", "value"}})
/// .Register(*registry);
///
/// ...
/// \endcode
///
/// \return An object of unspecified type T, i.e., an implementation detail
/// except that it has the following members:
///
/// - Name(const std::string&) to set the metric name,
/// - Help(const std::string&) to set an additional description.
/// - Label(const std::map<std::string, std::string>&) to assign a set of
/// key-value pairs (= labels) to the metric.
///
/// To finish the configuration of the Histogram metric register it with
/// Register(Registry&).
using BuildHistogram = Builder<Histogram<double>>;
} // namespace prometheus

View file

@ -0,0 +1,29 @@
#pragma once
#include <stdint.h>
#include "client_metric.h"
namespace prometheus {
class Metric {
public:
enum class Type {
Counter,
Gauge,
Summary,
Histogram,
Untyped,
};
Type type;
Metric (Type type_) : type(type_) {}
virtual ~Metric() = default;
virtual ClientMetric Collect() const = 0;
};
} // namespace prometheus

View file

@ -0,0 +1,18 @@
#pragma once
#include <string>
#include <vector>
#include "metric.h"
#include "prometheus/client_metric.h"
namespace prometheus {
struct MetricFamily {
Metric::Type type;
std::string name;
std::string help;
std::vector<ClientMetric> metric;
};
} // namespace prometheus

View file

@ -0,0 +1,86 @@
#pragma once
#include <thread>
#include <chrono>
#include <string>
#include "registry.h"
#include "text_serializer.h"
#include <jdl/httpclientlite.h>
namespace prometheus {
class PushToServer {
std::chrono::seconds period { 1 };
std::string uri { "" };
std::thread worker_thread { &PushToServer::worker_function, this };
std::shared_ptr<Registry> registry_ptr { nullptr };
bool must_die { false };
void push_data() {
if (registry_ptr) {
if (!uri.empty()) {
std::stringstream body_strm;
TextSerializer::Serialize(body_strm, registry_ptr->Collect());
std::string body = body_strm.str();
jdl::HTTPResponse response = jdl::HTTPClient::request(jdl::HTTPClient::m_post, jdl::URI(uri), body);
}
}
}
void worker_function() {
// it need for fast shutdown this thread when SaveToFile destructor is called
const uint64_t divider = 100;
uint64_t fraction = divider;
for (;;) {
std::chrono::milliseconds period_ms
= std::chrono::duration_cast<std::chrono::milliseconds>(period);
std::this_thread::sleep_for( period_ms / divider );
if (must_die) {
push_data();
return;
}
if (--fraction == 0) {
fraction = divider;
push_data();
}
}
}
public:
PushToServer() {
jdl::init_socket();
}
~PushToServer() {
must_die = true;
worker_thread.join();
jdl::deinit_socket();
}
PushToServer(std::shared_ptr<Registry>& registry_, const std::chrono::seconds& period_, const std::string& uri_) {
set_registry(registry_);
set_delay(period_);
set_uri(uri_);
}
void set_delay (const std::chrono::seconds& new_period) {
period = new_period;
}
void set_uri (const std::string& uri_) {
uri = std::move(uri_);
}
void set_registry (std::shared_ptr<Registry>& new_registry_ptr) {
registry_ptr = new_registry_ptr;
}
};
}

View file

@ -0,0 +1,123 @@
#pragma once
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "prometheus/collectable.h"
#include "prometheus/family.h"
namespace prometheus {
/// \brief Manages the collection of a number of metrics.
///
/// The Registry is responsible to expose data to a class/method/function
/// "bridge", which returns the metrics in a format Prometheus supports.
///
/// The key class is the Collectable. This has a method - called Collect() -
/// that returns zero or more metrics and their samples. The metrics are
/// represented by the class Family<>, which implements the Collectable
/// interface. A new metric is registered with BuildCounter(), BuildGauge(),
/// BuildHistogram() or BuildSummary().
///
/// The class is thread-safe. No concurrent call to any API of this type causes
/// a data race.
class Registry : public Collectable {
public:
/// \brief How to deal with repeatedly added family names for a type.
///
/// Adding a family with the same name but different types is always an error
/// and will lead to an exception.
enum class InsertBehavior {
/// \brief If a family with the same name and labels already exists return
/// the existing one. If no family with that name exists create it.
/// Otherwise throw.
Merge,
/// \brief Throws if a family with the same name already exists.
Throw,
/// \brief Never merge and always create a new family. This violates the
/// prometheus specification but was the default behavior in earlier
/// versions
NonStandardAppend,
};
using FamilyPtr = std::unique_ptr<Family>;
using Families = std::vector<FamilyPtr>;
const InsertBehavior insert_behavior;
mutable std::mutex mutex;
Families families;
/// \brief name Create a new registry.
///
/// \param insert_behavior How to handle families with the same name.
Registry (InsertBehavior insert_behavior_ = InsertBehavior::Merge)
: insert_behavior(insert_behavior_) {}
/// \brief Returns a list of metrics and their samples.
///
/// Every time the Registry is scraped it calls each of the metrics Collect
/// function.
///
/// \return Zero or more metrics and their samples.
virtual MetricFamilies Collect() const {
std::lock_guard<std::mutex> lock{ mutex };
MetricFamilies results;
for (const FamilyPtr& family_ptr : families) {
MetricFamilies metrics = family_ptr->Collect();
results.insert(results.end(), std::make_move_iterator(metrics.begin()), std::make_move_iterator(metrics.end()));
}
return results;
}
template <typename CustomFamily>
CustomFamily& Add (const std::string& name, const std::string& help, const Family::Labels& labels) {
std::lock_guard<std::mutex> lock{ mutex };
bool found_one_but_not_merge = false;
for (const FamilyPtr& family_ptr : families) {
if (family_ptr->GetName() == name) {
if (family_ptr->type != CustomFamily::static_type) // found family with this name and with different type
throw std::invalid_argument("Family name already exists with different type");
else { // found family with this name and the same type
switch (insert_behavior) {
case InsertBehavior::Throw:
throw std::invalid_argument("Family name already exists");
case InsertBehavior::Merge:
if (family_ptr->GetConstantLabels() == labels)
return dynamic_cast<CustomFamily&>(*family_ptr);
else // this strange rule was in previos version prometheus cpp
found_one_but_not_merge = true;
case InsertBehavior::NonStandardAppend:
continue;
}
}
}
}
if (found_one_but_not_merge) // this strange rule was in previos version prometheus cpp
throw std::invalid_argument("Family name already exists with different labels");
std::unique_ptr<CustomFamily> new_family_ptr (new CustomFamily(name, help, labels));
CustomFamily& new_family = *new_family_ptr;
families.push_back(std::move(new_family_ptr));
return new_family;
}
};
} // namespace prometheus

View file

@ -0,0 +1,83 @@
#pragma once
#include <thread>
#include <chrono>
#include <string>
#include <fstream>
#include <memory>
#include "registry.h"
#include "text_serializer.h"
namespace prometheus {
class SaveToFile {
std::chrono::seconds period { 1 };
std::string filename;
std::thread worker_thread { &SaveToFile::worker_function, this };
std::shared_ptr<Registry> registry_ptr { nullptr };
bool must_die { false };
void save_data() {
if (registry_ptr) {
std::fstream out_file_stream;
out_file_stream.open(filename, std::fstream::out | std::fstream::binary);
if (out_file_stream.is_open()) {
TextSerializer::Serialize(out_file_stream, registry_ptr->Collect());
out_file_stream.close();
}
}
}
void worker_function() {
// it need for fast shutdown this thread when SaveToFile destructor is called
const uint64_t divider = 100;
uint64_t fraction = divider;
for (;;) {
std::chrono::milliseconds period_ms
= std::chrono::duration_cast<std::chrono::milliseconds>(period);
std::this_thread::sleep_for( period_ms / divider );
if (must_die) {
save_data();
return;
}
if (--fraction == 0) {
fraction = divider;
save_data();
}
}
}
public:
SaveToFile() = default;
~SaveToFile() {
must_die = true;
worker_thread.join();
}
SaveToFile(std::shared_ptr<Registry>& registry_, const std::chrono::seconds& period_, const std::string& filename_) {
set_registry(registry_);
set_delay(period_);
set_out_file(filename_);
}
void set_delay (const std::chrono::seconds& new_period) {
period = new_period;
}
bool set_out_file (const std::string& filename_) {
filename = filename_;
std::fstream out_file_stream;
out_file_stream.open(filename, std::fstream::out | std::fstream::binary);
bool open_success = out_file_stream.is_open();
out_file_stream.close();
return open_success;
}
void set_registry (std::shared_ptr<Registry>& new_registry_ptr) {
registry_ptr = new_registry_ptr;
}
};
}

View file

@ -0,0 +1,154 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <mutex>
#include <vector>
#include "prometheus/metric.h"
#include "prometheus/family.h"
#include "prometheus/detail/ckms_quantiles.h"
#include "prometheus/detail/time_window_quantiles.h"
#include "prometheus/builder.h"
namespace prometheus {
/// \brief A summary metric samples observations over a sliding window of time.
///
/// This class represents the metric type summary:
/// https://prometheus.io/docs/instrumenting/writing_clientlibs/#summary
///
/// A summary provides a total count of observations and a sum of all observed
/// values. In contrast to a histogram metric it also calculates configurable
/// Phi-quantiles over a sliding window of time.
///
/// The essential difference between summaries and histograms is that summaries
/// calculate streaming Phi-quantiles on the client side and expose them
/// directly, while histograms expose bucketed observation counts and the
/// calculation of quantiles from the buckets of a histogram happens on the
/// server side:
/// https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile.
///
/// Note that Phi designates the probability density function of the standard
/// Gaussian distribution.
///
/// See https://prometheus.io/docs/practices/histograms/ for detailed
/// explanations of Phi-quantiles, summary usage, and differences to histograms.
///
/// The class is thread-safe. No concurrent call to any API of this type causes
/// a data race.
class Summary : Metric {
public:
using Value = double;
using Family = CustomFamily<Summary>;
static const Metric::Type static_type = Metric::Type::Summary;
using Quantiles = std::vector<detail::CKMSQuantiles::Quantile>;
const Quantiles quantiles_;
mutable std::mutex mutex_;
std::uint64_t count_;
double sum_;
detail::TimeWindowQuantiles quantile_values_;
public:
/// \brief Create a summary metric.
///
/// \param quantiles A list of 'targeted' Phi-quantiles. A targeted
/// Phi-quantile is specified in the form of a Phi-quantile and tolerated
/// error. For example a Quantile{0.5, 0.1} means that the median (= 50th
/// percentile) should be returned with 10 percent error or a Quantile{0.2,
/// 0.05} means the 20th percentile with 5 percent tolerated error. Note that
/// percentiles and quantiles are the same concept, except percentiles are
/// expressed as percentages. The Phi-quantile must be in the interval [0, 1].
/// Note that a lower tolerated error for a Phi-quantile results in higher
/// usage of resources (memory and cpu) to calculate the summary.
///
/// The Phi-quantiles are calculated over a sliding window of time. The
/// sliding window of time is configured by max_age and age_buckets.
///
/// \param max_age Set the duration of the time window, i.e., how long
/// observations are kept before they are discarded. The default value is 60
/// seconds.
///
/// \param age_buckets Set the number of buckets of the time window. It
/// determines the number of buckets used to exclude observations that are
/// older than max_age from the summary, e.g., if max_age is 60 seconds and
/// age_buckets is 5, buckets will be switched every 12 seconds. The value is
/// a trade-off between resources (memory and cpu for maintaining the bucket)
/// and how smooth the time window is moved. With only one age bucket it
/// effectively results in a complete reset of the summary each time max_age
/// has passed. The default value is 5.
Summary(const Quantiles& quantiles, std::chrono::milliseconds max_age = std::chrono::seconds{ 60 }, int age_buckets = 5)
: Metric(static_type), quantiles_{ quantiles }, count_{ 0 }, sum_{ 0 }, quantile_values_(quantiles_, max_age, age_buckets) {}
/// \brief Observe the given amount.
void Observe(const double value) {
std::lock_guard<std::mutex> lock(mutex_);
count_ += 1;
sum_ += value;
quantile_values_.insert(value);
}
/// \brief Get the current value of the summary.
///
/// Collect is called by the Registry when collecting metrics.
virtual ClientMetric Collect() const {
auto metric = ClientMetric{};
std::lock_guard<std::mutex> lock(mutex_);
metric.summary.quantile.reserve(quantiles_.size());
for (const auto& quantile : quantiles_) {
auto metricQuantile = ClientMetric::Quantile{};
metricQuantile.quantile = quantile.quantile;
metricQuantile.value = quantile_values_.get(quantile.quantile);
metric.summary.quantile.push_back(std::move(metricQuantile));
}
metric.summary.sample_count = count_;
metric.summary.sample_sum = sum_;
return metric;
}
};
/// \brief Return a builder to configure and register a Summary metric.
///
/// @copydetails Family<>::Family()
///
/// Example usage:
///
/// \code
/// auto registry = std::make_shared<Registry>();
/// auto& summary_family = prometheus::BuildSummary()
/// .Name("some_name")
/// .Help("Additional description.")
/// .Labels({{"key", "value"}})
/// .Register(*registry);
///
/// ...
/// \endcode
///
/// \return An object of unspecified type T, i.e., an implementation detail
/// except that it has the following members:
///
/// - Name(const std::string&) to set the metric name,
/// - Help(const std::string&) to set an additional description.
/// - Label(const std::map<std::string, std::string>&) to assign a set of
/// key-value pairs (= labels) to the metric.
///
/// To finish the configuration of the Summary metric register it with
/// Register(Registry&).
using BuildSummary = Builder<Summary>;
} // namespace prometheus

View file

@ -0,0 +1,211 @@
#pragma once
#include <iosfwd>
#include <vector>
#include <array>
#include <math.h>
#include <ostream>
#include "prometheus/metric_family.h"
#if __cpp_lib_to_chars >= 201611L
#include <charconv>
#endif
namespace prometheus {
class TextSerializer {
// Write a double as a string, with proper formatting for infinity and NaN
static void WriteValue (std::ostream& out, double value) {
if (std::isnan(value))
out << "Nan";
else if (std::isinf(value))
out << (value < 0 ? "-Inf" : "+Inf");
else {
std::array<char, 128> buffer;
#if __cpp_lib_to_chars >= 201611L
auto [ptr, ec] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value);
if (ec != std::errc()) {
throw std::runtime_error("Could not convert double to string: " +
std::make_error_code(ec).message());
}
out.write(buffer.data(), ptr - buffer.data());
#else
int wouldHaveWritten = std::snprintf(buffer.data(), buffer.size(), "%.*g", std::numeric_limits<double>::max_digits10 - 1, value);
if (wouldHaveWritten <= 0 || static_cast<std::size_t>(wouldHaveWritten) >= buffer.size()) {
throw std::runtime_error("Could not convert double to string");
}
out.write(buffer.data(), wouldHaveWritten);
#endif
}
}
static void WriteValue(std::ostream& out, const std::string& value) {
for (auto c : value) {
switch (c) {
case '\n': out << '\\' << 'n'; break;
case '\\': out << '\\' << c; break;
case '"': out << '\\' << c; break;
default: out << c; break;
}
}
}
// Write a line header: metric name and labels
template <typename T = std::string>
static void WriteHead(
std::ostream& out,
const MetricFamily& family,
const ClientMetric& metric,
const std::string& suffix = "",
const std::string& extraLabelName = "",
const T& extraLabelValue = T()) {
out << family.name << suffix;
if (!metric.label.empty() || !extraLabelName.empty()) {
out << "{";
const char* prefix = "";
for (auto& lp : metric.label) {
out << prefix << lp.name << "=\"";
WriteValue(out, lp.value);
out << "\"";
prefix = ",";
}
if (!extraLabelName.empty()) {
out << prefix << extraLabelName << "=\"";
WriteValue(out, extraLabelValue);
out << "\"";
}
out << "}";
}
out << " ";
}
// Write a line trailer: timestamp
static void WriteTail(std::ostream& out, const ClientMetric& metric) {
if (metric.timestamp_ms != 0) {
out << " " << metric.timestamp_ms;
}
out << "\n";
}
static void SerializeCounter(std::ostream& out, const MetricFamily& family, const ClientMetric& metric) {
WriteHead(out, family, metric);
WriteValue(out, metric.counter.value);
WriteTail(out, metric);
}
static void SerializeGauge(std::ostream& out, const MetricFamily& family, const ClientMetric& metric) {
WriteHead(out, family, metric);
WriteValue(out, metric.gauge.value);
WriteTail(out, metric);
}
static void SerializeSummary(std::ostream& out, const MetricFamily& family, const ClientMetric& metric) {
auto& sum = metric.summary;
WriteHead(out, family, metric, "_count");
out << sum.sample_count;
WriteTail(out, metric);
WriteHead(out, family, metric, "_sum");
WriteValue(out, sum.sample_sum);
WriteTail(out, metric);
for (auto& q : sum.quantile) {
WriteHead(out, family, metric, "", "quantile", q.quantile);
WriteValue(out, q.value);
WriteTail(out, metric);
}
}
static void SerializeUntyped(std::ostream& out, const MetricFamily& family, const ClientMetric& metric) {
WriteHead(out, family, metric);
WriteValue(out, metric.untyped.value);
WriteTail(out, metric);
}
static void SerializeHistogram(std::ostream& out, const MetricFamily& family, const ClientMetric& metric) {
auto& hist = metric.histogram;
WriteHead(out, family, metric, "_count");
out << hist.sample_count;
WriteTail(out, metric);
WriteHead(out, family, metric, "_sum");
WriteValue(out, hist.sample_sum);
WriteTail(out, metric);
double last = -std::numeric_limits<double>::infinity();
for (auto& b : hist.bucket) {
WriteHead(out, family, metric, "_bucket", "le", b.upper_bound);
last = b.upper_bound;
out << b.cumulative_count;
WriteTail(out, metric);
}
if (last != std::numeric_limits<double>::infinity()) {
WriteHead(out, family, metric, "_bucket", "le", "+Inf");
out << hist.sample_count;
WriteTail(out, metric);
}
}
static void SerializeFamily(std::ostream& out, const MetricFamily& family) {
if (!family.help.empty()) {
out << "# HELP " << family.name << " " << family.help << "\n";
}
switch (family.type) {
case Metric::Type::Counter:
out << "# TYPE " << family.name << " counter\n";
for (auto& metric : family.metric) {
SerializeCounter(out, family, metric);
}
break;
case Metric::Type::Gauge:
out << "# TYPE " << family.name << " gauge\n";
for (auto& metric : family.metric) {
SerializeGauge(out, family, metric);
}
break;
case Metric::Type::Summary:
out << "# TYPE " << family.name << " summary\n";
for (auto& metric : family.metric) {
SerializeSummary(out, family, metric);
}
break;
case Metric::Type::Untyped:
out << "# TYPE " << family.name << " untyped\n";
for (auto& metric : family.metric) {
SerializeUntyped(out, family, metric);
}
break;
case Metric::Type::Histogram:
out << "# TYPE " << family.name << " histogram\n";
for (auto& metric : family.metric) {
SerializeHistogram(out, family, metric);
}
break;
}
}
public:
static void Serialize (std::ostream& out, const std::vector<MetricFamily>& metrics) {
std::locale saved_locale = out.getloc();
out.imbue(std::locale::classic());
for (auto& family : metrics) {
SerializeFamily(out, family);
}
out.imbue(saved_locale);
}
};
} // namespace prometheus

View file

@ -0,0 +1,62 @@
#pragma once
#include <chrono>
#include <cstddef>
#include <vector>
#include "prometheus/detail/ckms_quantiles.h"
namespace prometheus {
namespace detail {
class TimeWindowQuantiles {
using Clock = std::chrono::steady_clock;
public:
TimeWindowQuantiles(const std::vector<CKMSQuantiles::Quantile>& quantiles,
const Clock::duration max_age, const int age_buckets)
: quantiles_(quantiles),
ckms_quantiles_(age_buckets, CKMSQuantiles(quantiles_)),
current_bucket_(0),
last_rotation_(Clock::now()),
rotation_interval_(max_age / age_buckets) {}
double get(double q) const {
CKMSQuantiles& current_bucket = rotate();
return current_bucket.get(q);
}
void insert(double value) {
rotate();
for (auto& bucket : ckms_quantiles_) {
bucket.insert(value);
}
}
private:
CKMSQuantiles& rotate() const {
auto delta = Clock::now() - last_rotation_;
while (delta > rotation_interval_) {
ckms_quantiles_[current_bucket_].reset();
if (++current_bucket_ >= ckms_quantiles_.size()) {
current_bucket_ = 0;
}
delta -= rotation_interval_;
last_rotation_ += rotation_interval_;
}
return ckms_quantiles_[current_bucket_];
}
const std::vector<CKMSQuantiles::Quantile>& quantiles_;
mutable std::vector<CKMSQuantiles> ckms_quantiles_;
mutable std::size_t current_bucket_;
mutable Clock::time_point last_rotation_;
const Clock::duration rotation_interval_;
};
} // namespace detail
} // namespace prometheus