mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-03-12 04:36:29 -07:00
357 lines
13 KiB
C++
357 lines
13 KiB
C++
#pragma once
|
|
|
|
#include <cstddef>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <cassert>
|
|
#include <stdexcept>
|
|
|
|
#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
|