Commit 00ee8640 authored by Dominik Charousset's avatar Dominik Charousset

Implement Prometheus metric exposition

parent cfc0baa5
...@@ -162,6 +162,7 @@ add_library(libcaf_core_obj OBJECT ${CAF_CORE_HEADERS} ...@@ -162,6 +162,7 @@ add_library(libcaf_core_obj OBJECT ${CAF_CORE_HEADERS}
src/stream_priority_strings.cpp src/stream_priority_strings.cpp
src/string_algorithms.cpp src/string_algorithms.cpp
src/string_view.cpp src/string_view.cpp
src/telemetry/collector/prometheus.cpp
src/telemetry/label.cpp src/telemetry/label.cpp
src/telemetry/label_view.cpp src/telemetry/label_view.cpp
src/telemetry/metric.cpp src/telemetry/metric.cpp
...@@ -307,6 +308,7 @@ caf_add_test_suites(caf-core-test ...@@ -307,6 +308,7 @@ caf_add_test_suites(caf-core-test
string_algorithms string_algorithms
string_view string_view
sum_type sum_type
telemetry.collector.prometheus
telemetry.int_gauge telemetry.int_gauge
telemetry.label telemetry.label
telemetry.metric_registry telemetry.metric_registry
......
...@@ -289,6 +289,14 @@ inline std::string to_string(string_view x) { ...@@ -289,6 +289,14 @@ inline std::string to_string(string_view x) {
} // namespace caf } // namespace caf
namespace caf::literals {
constexpr string_view operator""_sv(const char* cstr, size_t len) {
return {cstr, len};
}
} // namespace caf::literals
namespace std { namespace std {
CAF_CORE_EXPORT std::ostream& operator<<(std::ostream& out, caf::string_view); CAF_CORE_EXPORT std::ostream& operator<<(std::ostream& out, caf::string_view);
......
/******************************************************************************
* ____ _ _____ *
* / ___| / \ | ___| C++ *
* | | / _ \ | |_ Actor *
* | |___ / ___ \| _| Framework *
* \____/_/ \_|_| *
* *
* Copyright 2011-2020 Dominik Charousset *
* *
* Distributed under the terms and conditions of the BSD 3-Clause License or *
* (at your option) under the terms and conditions of the Boost Software *
* License 1.0. See accompanying files LICENSE and LICENSE_ALTERNATIVE. *
* *
* If you did not receive a copy of the license files, see *
* http://opensource.org/licenses/BSD-3-Clause and *
* http://www.boost.org/LICENSE_1_0.txt. *
******************************************************************************/
#pragma once
#include <unordered_map>
#include <vector>
#include "caf/fwd.hpp"
#include "caf/string_view.hpp"
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 {
public:
/// A buffer for storing UTF-8 characters. Using a vector instead of a
/// `std::string` has slight performance benefits, since the vector does not
/// have to maintain a null-terminator.
using char_buffer = std::vector<char>;
/// Initializes state before passing this collector to a metric registry.
void begin_collecting();
/// Finalizes state after collecting metrics from a registry completed.
void end_collecting();
/// Applies this collector to the registry, filling the character buffer while
/// collecting metrics.
string_view collect_from(const metric_registry& registry);
// -- call operators for the metric registry ---------------------------------
void operator()(const metric_family* family, const metric* instance,
const int_gauge* gauge);
private:
/// Sets `current_family_` if not pointing to `family` already. When setting
/// the member variable, also writes meta information to `buf_`.
void set_current_family(const metric_family* family,
string_view prometheus_type);
/// Stores the generated text output.
char_buffer buf_;
/// Caches type information and help text for a metric.
std::unordered_map<const metric_family*, char_buffer> meta_info_;
/// Caches which metric family is currently collected.
const metric_family* current_family_ = nullptr;
};
} // namespace caf::telemetry::collector
/******************************************************************************
* ____ _ _____ *
* / ___| / \ | ___| C++ *
* | | / _ \ | |_ Actor *
* | |___ / ___ \| _| Framework *
* \____/_/ \_|_| *
* *
* Copyright 2011-2020 Dominik Charousset *
* *
* Distributed under the terms and conditions of the BSD 3-Clause License or *
* (at your option) under the terms and conditions of the Boost Software *
* License 1.0. See accompanying files LICENSE and LICENSE_ALTERNATIVE. *
* *
* If you did not receive a copy of the license files, see *
* http://opensource.org/licenses/BSD-3-Clause and *
* http://www.boost.org/LICENSE_1_0.txt. *
******************************************************************************/
#include "caf/telemetry/collector/prometheus.hpp"
#include <cmath>
#include "caf/telemetry/int_gauge.hpp"
#include "caf/telemetry/metric.hpp"
#include "caf/telemetry/metric_family.hpp"
#include "caf/telemetry/metric_registry.hpp"
using namespace caf::literals;
namespace caf::telemetry::collector {
namespace {
void append(prometheus::char_buffer&) {
// End of recursion.
}
template <class... Ts>
void append(prometheus::char_buffer&, string_view, Ts&&...);
template <class... Ts>
void append(prometheus::char_buffer&, char, Ts&&...);
template <class... Ts>
void append(prometheus::char_buffer&, double, Ts&&...);
template <class... Ts>
void append(prometheus::char_buffer&, int64_t, Ts&&...);
template <class... Ts>
void append(prometheus::char_buffer&, const metric_family*, Ts&&...);
template <class... Ts>
void append(prometheus::char_buffer&, const metric*, Ts&&...);
template <class... Ts>
void append(prometheus::char_buffer& buf, string_view str, Ts&&... xs) {
buf.insert(buf.end(), str.begin(), str.end());
append(buf, std::forward<Ts>(xs)...);
}
template <class... Ts>
void append(prometheus::char_buffer& buf, char ch, Ts&&... xs) {
buf.emplace_back(ch);
append(buf, std::forward<Ts>(xs)...);
}
template <class... Ts>
void append(prometheus::char_buffer& buf, double val, Ts&&... xs) {
if (isnan(val)) {
append(buf, "NaN"_sv);
} else if (isinf(val)) {
if (signbit(val))
append(buf, "+Inf"_sv);
else
append(buf, "-Inf"_sv);
} else {
append(buf, std::to_string(val));
}
append(buf, std::forward<Ts>(xs)...);
}
template <class... Ts>
void append(prometheus::char_buffer& buf, int64_t val, Ts&&... xs) {
append(buf, std::to_string(val));
append(buf, std::forward<Ts>(xs)...);
}
template <class... Ts>
void append(prometheus::char_buffer& buf, const metric_family* family,
Ts&&... xs) {
append(buf, family->prefix(), '_', family->name());
if (family->unit() != "1"_sv)
append(buf, '_', family->unit());
if (family->is_sum())
append(buf, "_total"_sv);
append(buf, std::forward<Ts>(xs)...);
}
template <class... Ts>
void append(prometheus::char_buffer& buf, const metric* instance, Ts&&... xs) {
const auto& labels = instance->labels();
if (!labels.empty()) {
append(buf, '{');
auto i = labels.begin();
append(buf, i->name(), "=\""_sv, i->value(), '"');
while (++i != labels.end())
append(buf, ',', i->name(), "=\"", i->value(), '"');
append(buf, '}');
}
append(buf, std::forward<Ts>(xs)...);
}
} // namespace
string_view prometheus::collect_from(const metric_registry& registry) {
buf_.clear();
registry.collect(*this);
current_family_ = nullptr;
return {buf_.data(), buf_.size()};
}
void prometheus::operator()(const metric_family* family, const metric* instance,
const int_gauge* gauge) {
set_current_family(family, "gauge");
append(buf_, family, instance, ' ', gauge->value(), '\n');
}
void prometheus::set_current_family(const metric_family* family,
string_view prometheus_type) {
if (current_family_ == family)
return;
current_family_ = family;
auto i = meta_info_.find(family);
if (i == meta_info_.end()) {
i = meta_info_.emplace(family, char_buffer{}).first;
if (!family->helptext().empty())
append(i->second, "# HELP ", family, ' ', family->helptext(), '\n');
append(i->second, "# TYPE ", family, ' ', prometheus_type, '\n');
}
buf_.insert(buf_.end(), i->second.begin(), i->second.end());
}
} // namespace caf::telemetry::collector
/******************************************************************************
* ____ _ _____ *
* / ___| / \ | ___| C++ *
* | | / _ \ | |_ Actor *
* | |___ / ___ \| _| Framework *
* \____/_/ \_|_| *
* *
* Copyright 2011-2020 Dominik Charousset *
* *
* Distributed under the terms and conditions of the BSD 3-Clause License or *
* (at your option) under the terms and conditions of the Boost Software *
* License 1.0. See accompanying files LICENSE and LICENSE_ALTERNATIVE. *
* *
* If you did not receive a copy of the license files, see *
* http://opensource.org/licenses/BSD-3-Clause and *
* http://www.boost.org/LICENSE_1_0.txt. *
******************************************************************************/
#define CAF_SUITE telemetry.collector.prometheus
#include "caf/telemetry/collector/prometheus.hpp"
#include "caf/test/dsl.hpp"
#include "caf/telemetry/metric_registry.hpp"
#include "caf/telemetry/metric_type.hpp"
using namespace caf;
using namespace caf::literals;
using namespace caf::telemetry;
namespace {
struct fixture {
collector::prometheus exporter;
metric_registry registry;
};
} // namespace
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);
CAF_CHECK_EQUAL(exporter.collect_from(registry),
R"(# HELP foo_bar_seconds Just some value without labels.
# TYPE foo_bar_seconds gauge
foo_bar_seconds 123
# HELP some_value_total Just some (total) value with two labels.
# TYPE some_value_total gauge
some_value_total{a="1",b="2"} 12
some_value_total{a="2",b="1"} 21
# TYPE other_value_seconds_total gauge
other_value_seconds_total{x="true"} 31337
)"_sv);
CAF_MESSAGE("multiple runs generate the same output");
std::string res1;
{
auto buf = exporter.collect_from(registry);
res1.assign(buf.begin(), buf.end());
}
CAF_CHECK_EQUAL(res1, exporter.collect_from(registry));
}
CAF_TEST_FIXTURE_SCOPE_END()
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