Commit f20b13b0 authored by Dominik Charousset's avatar Dominik Charousset

Revise the metric family API

parent 6b1bd132
......@@ -22,6 +22,7 @@
#include <unordered_map>
#include <vector>
#include "caf/detail/core_export.hpp"
#include "caf/fwd.hpp"
#include "caf/string_view.hpp"
......@@ -29,7 +30,7 @@ namespace caf::telemetry::collector {
/// Collects system metrics and exports them to the text-based Prometheus
/// format. For a documentation of the format, see: https://git.io/fjgDD.
class prometheus {
class CAF_CORE_EXPORT prometheus {
public:
// -- member types -----------------------------------------------------------
......
......@@ -23,12 +23,16 @@
#include <atomic>
#include <cstdint>
#include "caf/telemetry/metric_type.hpp"
namespace caf::telemetry {
/// A metric that represents a single integer value that can arbitrarily go up
/// and down.
class CAF_CORE_EXPORT int_gauge {
public:
static constexpr metric_type runtime_type = metric_type::int_gauge;
int_gauge() noexcept : value_(0) {
// nop
}
......@@ -38,22 +42,22 @@ public:
}
/// Increments the gauge by 1.
void increment() noexcept {
void inc() noexcept {
++value_;
}
/// Increments the gauge by `amount`.
void increment(int64_t amount) noexcept {
void inc(int64_t amount) noexcept {
value_.fetch_add(amount);
}
/// Decrements the gauge by 1.
void decrement() noexcept {
void dec() noexcept {
--value_;
}
/// Decrements the gauge by `amount`.
void decrement(int64_t amount) noexcept {
void dec(int64_t amount) noexcept {
value_.fetch_sub(amount);
}
......
......@@ -77,6 +77,8 @@ public:
return is_sum_;
}
virtual metric_type type() const noexcept = 0;
private:
std::string prefix_;
std::string name_;
......
......@@ -19,9 +19,12 @@
#pragma once
#include <algorithm>
#include <initializer_list>
#include <memory>
#include <mutex>
#include "caf/span.hpp"
#include "caf/string_view.hpp"
#include "caf/telemetry/label.hpp"
#include "caf/telemetry/label_view.hpp"
#include "caf/telemetry/metric.hpp"
......@@ -33,27 +36,34 @@ namespace caf::telemetry {
template <class Type>
class metric_family_impl : public metric_family {
public:
using metric_type = metric_impl<Type>;
using impl_type = metric_impl<Type>;
using metric_family::metric_family;
Type* get_or_add(const std::vector<label_view>& labels) {
auto has_label_values = [&labels](const auto& metric_ptr) {
metric_type type() const noexcept override {
return Type::runtime_type;
}
Type* get_or_add(span<const label_view> labels) {
auto has_label_values = [labels](const auto& metric_ptr) {
const auto& metric_labels = metric_ptr->labels();
for (size_t index = 0; index < labels.size(); ++index)
if (labels[index].value() != metric_labels[index].value())
return false;
return true;
return std::is_permutation(metric_labels.begin(), metric_labels.end(),
labels.begin(), labels.end());
};
std::unique_lock<std::mutex> guard{mx_};
auto m = std::find_if(metrics_.begin(), metrics_.end(), has_label_values);
if (m == metrics_.end()) {
std::vector<label> cpy{labels.begin(), labels.end()};
m = metrics_.emplace(m, std::make_unique<metric_type>(std::move(cpy)));
std::sort(cpy.begin(), cpy.end());
m = metrics_.emplace(m, std::make_unique<impl_type>(std::move(cpy)));
}
return std::addressof(m->get()->impl());
}
Type* get_or_add(std::initializer_list<label_view> labels) {
return get_or_add(span<const label_view>{labels.begin(), labels.size()});
}
template <class Collector>
void collect(Collector& collector) const {
std::unique_lock<std::mutex> guard{mx_};
......@@ -63,7 +73,7 @@ public:
private:
mutable std::mutex mx_;
std::vector<std::unique_ptr<metric_type>> metrics_;
std::vector<std::unique_ptr<impl_type>> metrics_;
};
} // namespace caf::telemetry
......@@ -18,6 +18,7 @@
#pragma once
#include <initializer_list>
#include <map>
#include <memory>
#include <mutex>
......@@ -42,26 +43,8 @@ public:
~metric_registry();
/// Adds a new metric family to the registry.
/// @param type The kind of the metric, e.g. gauge or count.
/// @param prefix The prefix (namespace) this family belongs to. Usually the
/// application or protocol name, e.g., `http`. The prefix `caf`
/// as well as prefixes starting with an underscore are
/// reserved.
/// @param name The human-readable name of the metric, e.g., `requests`.
/// @param label_names Names for all label dimensions of the metric.
/// @param helptext Short explanation of the metric.
/// @param unit Unit of measurement. Please use base units such as `bytes` or
/// `seconds` (prefer lowercase). The pseudo-unit `1` identifies
/// dimensionless counts.
/// @param is_sum Setting this to `true` indicates that this metric adds
/// something up to a total, where only the total value is of
/// interest. For example, the total number of HTTP requests.
void add_family(metric_type type, std::string prefix, std::string name,
std::vector<std::string> label_names, std::string helptext,
std::string unit = "1", bool is_sum = false);
/// Adds a new metric family to the registry.
/// Returns a metric family. Creates the family lazily if necessary, but fails
/// if the full name already belongs to a different family.
/// @param prefix The prefix (namespace) this family belongs to. Usually the
/// application or protocol name, e.g., `http`. The prefix `caf`
/// as well as prefixes starting with an underscore are
......@@ -76,25 +59,40 @@ public:
/// something up to a total, where only the total value is of
/// interest. For example, the total number of HTTP requests.
template <class Type>
metric_family_impl<Type>*
add_family(std::string prefix, std::string name,
std::vector<std::string> label_names, std::string helptext,
std::string unit = "1", bool is_sum = false) {
metric_family_impl<Type>* family(string_view prefix, string_view name,
span<const string_view> label_names,
string_view helptext, string_view unit = "1",
bool is_sum = false) {
using family_type = metric_family_impl<Type>;
std::sort(label_names.begin(), label_names.end());
auto ptr = std::make_unique<family_type>(std::move(prefix), std::move(name),
std::move(label_names),
std::move(helptext),
std::move(unit), is_sum);
std::unique_lock<std::mutex> guard{families_mx_};
assert_unregistered(ptr->prefix(), ptr->name());
if (auto ptr = fetch(prefix, name)) {
assert_properties(ptr, Type::runtime_type, label_names, unit, is_sum);
return static_cast<family_type*>(ptr);
}
auto ptr = std::make_unique<family_type>(to_string(prefix), to_string(name),
to_sorted_vec(label_names),
to_string(helptext),
to_string(unit), is_sum);
auto& families = container_by_type<Type>();
families.emplace_back(std::move(ptr));
return families.back().get();
}
/// Adds a new metric singleton, i.e., a family without label dimensions and
/// thus exactly one metric instance.
/// @copydoc family
template <class Type>
metric_family_impl<Type>*
family(string_view prefix, string_view name,
std::initializer_list<string_view> label_names, string_view helptext,
string_view unit = "1", bool is_sum = false) {
return family<Type>(prefix, name,
span<const string_view>{label_names.begin(),
label_names.size()},
helptext, unit, is_sum);
}
/// Returns a metric singleton, i.e., the single instance of a family without
/// label dimensions. Creates all objects lazily if necessary, but fails if
/// the full name already belongs to a different family.
/// @param prefix The prefix (namespace) this family belongs to. Usually the
/// application or protocol name, e.g., `http`. The prefix `caf`
/// as well as prefixes starting with an underscore are
......@@ -108,17 +106,12 @@ public:
/// something up to a total, where only the total value is of
/// interest. For example, the total number of HTTP requests.
template <class Type>
Type* add_singleton(std::string prefix, std::string name,
std::string helptext, std::string unit = "1",
bool is_sum = false) {
auto fptr = add_family<Type>(std::move(prefix), std::move(name), {},
std::move(helptext), std::move(unit), is_sum);
Type* singleton(string_view prefix, string_view name, string_view helptext,
string_view unit = "1", bool is_sum = false) {
auto fptr = family<Type>(prefix, name, {}, helptext, unit, is_sum);
return fptr->get_or_add({});
}
telemetry::int_gauge* int_gauge(string_view prefix, string_view name,
std::vector<label_view> labels);
template <class Collector>
void collect(Collector& collector) const {
std::unique_lock<std::mutex> guard{families_mx_};
......@@ -128,7 +121,22 @@ public:
private:
/// @pre `families_mx_` is locked.
void assert_unregistered(const std::string& prefix, const std::string& name);
metric_family* fetch(const string_view& prefix, const string_view& name);
static std::vector<std::string> to_sorted_vec(span<const string_view> xs) {
std::vector<std::string> result;
if (!xs.empty()) {
result.reserve(xs.size());
for (auto x : xs)
result.emplace_back(to_string(x));
std::sort(result.begin(), result.end());
}
return result;
}
void assert_properties(metric_family* ptr, metric_type type,
span<const string_view> label_names, string_view unit,
bool is_sum);
template <class Type>
metric_family_container<Type>& container_by_type();
......
......@@ -49,7 +49,7 @@ actor_registry::~actor_registry() {
}
actor_registry::actor_registry(actor_system& sys) : system_(sys) {
running_ = sys.telemetry().add_singleton<telemetry::int_gauge>(
running_ = sys.telemetry().singleton<telemetry::int_gauge>(
"caf", "running_actors", "Number of currently running actors.");
}
......
......@@ -35,74 +35,35 @@ metric_registry::~metric_registry() {
// nop
}
telemetry::int_gauge*
metric_registry::int_gauge(string_view prefix, string_view name,
std::vector<label_view> labels) {
// Make sure labels are sorted by name.
auto cmp = [](const label_view& x, const label_view& y) {
return x.name() < y.name();
};
std::sort(labels.begin(), labels.end(), cmp);
// Fetch the family.
auto matches = [&](const auto& family) {
auto lbl_cmp = [](const label_view& lbl, string_view name) {
return lbl.name() == name;
};
return family->prefix() == prefix && family->name() == name
&& std::equal(labels.begin(), labels.end(),
family->label_names().begin(),
family->label_names().end(), lbl_cmp);
metric_family* metric_registry::fetch(const string_view& prefix,
const string_view& name) {
auto matches = [&](const auto& ptr) {
return ptr->prefix() == prefix && ptr->name() == name;
};
using family_type = metric_family_impl<telemetry::int_gauge>;
family_type* fptr;
{
std::unique_lock<std::mutex> guard{families_mx_};
auto i = std::find_if(int_gauges_.begin(), int_gauges_.end(), matches);
if (i == int_gauges_.end()) {
std::string prefix_str{prefix.begin(), prefix.end()};
std::string name_str{name.begin(), name.end()};
assert_unregistered(prefix_str, name_str);
std::vector<std::string> label_names;
label_names.reserve(labels.size());
for (auto& lbl : labels) {
auto lbl_name = lbl.name();
label_names.emplace_back(std::string{lbl_name.begin(), lbl_name.end()});
}
auto ptr = std::make_unique<family_type>(std::move(prefix_str),
std::move(name_str),
std::move(label_names), "", "1",
false);
i = int_gauges_.emplace(i, std::move(ptr));
}
fptr = i->get();
}
return fptr->get_or_add(labels);
if (auto i = std::find_if(int_gauges_.begin(), int_gauges_.end(), matches);
i != int_gauges_.end())
return i->get();
return nullptr;
}
void metric_registry::add_family(metric_type type, std::string prefix,
std::string name,
std::vector<std::string> label_names,
std::string helptext, std::string unit,
bool is_sum) {
namespace t = caf::telemetry;
switch (type) {
case metric_type::int_gauge:
add_family<t::int_gauge>(std::move(prefix), std::move(name),
std::move(label_names), std::move(helptext),
std::move(unit), is_sum);
break;
default:
CAF_RAISE_ERROR("invalid metric type");
}
}
void metric_registry::assert_unregistered(const std::string& prefix,
const std::string& name) {
auto matches = [&](const auto& ptr) {
return ptr->prefix() == prefix && ptr->name() == name;
void metric_registry::assert_properties(metric_family* ptr, metric_type type,
span<const string_view> label_names,
string_view unit, bool is_sum) {
auto labels_match = [&] {
const auto& xs = ptr->label_names();
const auto& ys = label_names;
return std::is_sorted(ys.begin(), ys.end())
? std::equal(xs.begin(), xs.end(), ys.begin(), ys.end())
: std::is_permutation(xs.begin(), xs.end(), ys.begin(), ys.end());
};
if (std::any_of(int_gauges_.begin(), int_gauges_.end(), matches))
CAF_RAISE_ERROR("prefix and name already belong to an int gauge family");
if (ptr->type() != type)
CAF_RAISE_ERROR("full name with different metric type found");
if (!labels_match())
CAF_RAISE_ERROR("full name with different label dimensions found");
if (ptr->unit() != unit)
CAF_RAISE_ERROR("full name with different unit found");
if (ptr->is_sum() != is_sum)
CAF_RAISE_ERROR("full name with different is-sum flag found");
}
} // namespace caf::telemetry
......@@ -32,6 +32,7 @@ using namespace caf::telemetry;
namespace {
struct fixture {
using ig = int_gauge;
collector::prometheus exporter;
metric_registry registry;
};
......@@ -41,21 +42,21 @@ struct fixture {
CAF_TEST_FIXTURE_SCOPE(prometheus_tests, fixture)
CAF_TEST(the Prometheus collector generates text output) {
registry.add_family(metric_type::int_gauge, "foo", "bar", {},
"Just some value without labels.", "seconds");
registry.add_family(metric_type::int_gauge, "some", "value", {"a", "b"},
"Just some (total) value with two labels.", "1", true);
registry.add_family(metric_type::int_gauge, "other", "value", {"x"}, "",
"seconds", true);
registry.int_gauge("foo", "bar", {})->value(123);
registry.int_gauge("some", "value", {{"a", "1"}, {"b", "2"}})->value(12);
registry.int_gauge("some", "value", {{"b", "1"}, {"a", "2"}})->value(21);
registry.int_gauge("other", "value", {{"x", "true"}})->value(31337);
auto fb = registry.family<ig>("foo", "bar", {}, "Some value without labels.",
"seconds");
auto sv = registry.family<ig>("some", "value", {"a", "b"},
"Some (total) value with two labels.", "1",
true);
auto ov = registry.family<ig>("other", "value", {"x"}, "", "seconds", true);
fb->get_or_add({})->value(123);
sv->get_or_add({{"a", "1"}, {"b", "2"}})->value(12);
sv->get_or_add({{"b", "1"}, {"a", "2"}})->value(21);
ov->get_or_add({{"x", "true"}})->value(31337);
CAF_CHECK_EQUAL(exporter.collect_from(registry, 42),
R"(# HELP foo_bar_seconds Just some value without labels.
R"(# HELP foo_bar_seconds Some value without labels.
# TYPE foo_bar_seconds gauge
foo_bar_seconds 123 42
# HELP some_value_total Just some (total) value with two labels.
# HELP some_value_total Some (total) value with two labels.
# TYPE some_value_total gauge
some_value_total{a="1",b="2"} 12 42
some_value_total{a="2",b="1"} 21 42
......
......@@ -29,12 +29,12 @@ CAF_TEST(integer gauges can increment and decrement) {
CAF_MESSAGE("gauges start at 0");
CAF_CHECK_EQUAL(g.value(), 0);
CAF_MESSAGE("gauges are incrementable");
g.increment();
g.increment(2);
g.inc();
g.inc(2);
CAF_CHECK_EQUAL(g.value(), 3);
CAF_MESSAGE("gauges are decrementable");
g.decrement();
g.decrement(5);
g.dec();
g.dec(5);
CAF_CHECK_EQUAL(g.value(), -3);
CAF_MESSAGE("gauges allow setting values");
g.value(42);
......
......@@ -32,7 +32,7 @@ using namespace caf::telemetry;
namespace {
struct collector {
struct test_collector {
std::string result;
void operator()(const metric_family* family, const metric* instance,
......@@ -70,19 +70,9 @@ struct collector {
};
struct fixture {
using ig = int_gauge;
metric_registry registry;
auto bind(string_view prefix, string_view name) {
struct bound {
fixture* self_;
string_view prefix_;
string_view name_;
auto int_gauge(std::vector<label_view> labels) {
return self_->registry.int_gauge(prefix_, name_, std::move(labels));
}
};
return bound{this, prefix, name};
}
test_collector collector;
};
} // namespace
......@@ -90,46 +80,53 @@ struct fixture {
CAF_TEST_FIXTURE_SCOPE(metric_registry_tests, fixture)
CAF_TEST(registries lazily create metrics) {
registry.add_family(metric_type::int_gauge, "caf", "running_actors",
{"var1", "var2"},
auto f = registry.family<ig>("caf", "running_actors", {"var1", "var2"},
"How many actors are currently running?");
auto var = bind("caf", "running_actors");
std::vector<label_view> v1{{"var1", "foo"}, {"var2", "bar"}};
std::vector<label_view> v1_reversed{{"var2", "bar"}, {"var1", "foo"}};
std::vector<label_view> v2{{"var1", "bar"}, {"var2", "foo"}};
std::vector<label_view> v2_reversed{{"var2", "foo"}, {"var1", "bar"}};
var.int_gauge(v1)->value(42);
var.int_gauge(v2)->value(23);
CAF_CHECK_EQUAL(var.int_gauge(v1)->value(), 42);
CAF_CHECK_EQUAL(var.int_gauge(v1_reversed)->value(), 42);
CAF_CHECK_EQUAL(var.int_gauge(v2)->value(), 23);
CAF_CHECK_EQUAL(var.int_gauge(v2_reversed)->value(), 23);
f->get_or_add(v1)->value(42);
f->get_or_add(v2)->value(23);
CAF_CHECK_EQUAL(f->get_or_add(v1)->value(), 42);
CAF_CHECK_EQUAL(f->get_or_add(v1_reversed)->value(), 42);
CAF_CHECK_EQUAL(f->get_or_add(v2)->value(), 23);
CAF_CHECK_EQUAL(f->get_or_add(v2_reversed)->value(), 23);
}
CAF_TEST(registries allow users to collect all registered metrics) {
registry.add_family(metric_type::int_gauge, "foo", "bar", {},
"Just some value without labels.", "seconds");
registry.add_family(metric_type::int_gauge, "some", "value", {"a", "b"},
"Just some (total) value with two labels.", "1", true);
registry.add_family(metric_type::int_gauge, "other", "value", {"x"},
"Just some (total) seconds value with a labels.",
auto fb = registry.family<ig>("foo", "bar", {}, "Some value without labels.",
"seconds");
auto sv = registry.family<ig>("some", "value", {"a", "b"},
"Some (total) value with two labels.", "1",
true);
auto ov = registry.family<ig>("other", "value", {"x"},
"Some (total) seconds with one label.",
"seconds", true);
registry.add_family(metric_type::int_gauge, "caf", "running_actors", {"node"},
"How many actors are currently running?");
registry.int_gauge("foo", "bar", {})->value(123);
registry.int_gauge("some", "value", {{"a", "1"}, {"b", "2"}})->value(12);
registry.int_gauge("some", "value", {{"b", "1"}, {"a", "2"}})->value(21);
registry.int_gauge("other", "value", {{"x", "true"}})->value(31337);
auto running_actors = bind("caf", "running_actors");
running_actors.int_gauge({{"node", "localhost"}})->value(42);
// Note: not registering caf_mailbox_size previously forces CAF to create it
// lazily when adding the first gauge.
auto mailbox_size = bind("caf", "mailbox_size");
mailbox_size.int_gauge({{"name", "printer"}})->value(3);
mailbox_size.int_gauge({{"name", "parser"}})->value(12);
collector c;
registry.collect(c);
CAF_CHECK_EQUAL(c.result, R"(
auto ra = registry.family<ig>("caf", "running_actors", {"node"},
"How many actors are running?");
auto ms = registry.family<ig>("caf", "mailbox_size", {"name"},
"How full is the mailbox?");
CAF_MESSAGE("the registry always returns the same family object");
CAF_CHECK_EQUAL(fb, registry.family<ig>("foo", "bar", {}, "", "seconds"));
CAF_CHECK_EQUAL(sv, registry.family<ig>("some", "value", {"a", "b"}, "", "1",
true));
CAF_CHECK_EQUAL(sv, registry.family<ig>("some", "value", {"b", "a"}, "", "1",
true));
CAF_MESSAGE("families always return the same metric object for given labels");
CAF_CHECK_EQUAL(fb->get_or_add({}), fb->get_or_add({}));
CAF_CHECK_EQUAL(sv->get_or_add({{"a", "1"}, {"b", "2"}}),
sv->get_or_add({{"b", "2"}, {"a", "1"}}));
CAF_MESSAGE("collectors can observe all metrics in the registry");
fb->get_or_add({})->inc(123);
sv->get_or_add({{"a", "1"}, {"b", "2"}})->value(12);
sv->get_or_add({{"b", "1"}, {"a", "2"}})->value(21);
ov->get_or_add({{"x", "true"}})->value(31337);
ra->get_or_add({{"node", "localhost"}})->value(42);
ms->get_or_add({{"name", "printer"}})->value(3);
ms->get_or_add({{"name", "parser"}})->value(12);
registry.collect(collector);
CAF_CHECK_EQUAL(collector.result, R"(
foo_bar_seconds 123
some_value_total{a="1",b="2"} 12
some_value_total{a="2",b="1"} 21
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment