diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 00000000..501b9172 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,6 @@ +build:asan --strip=never +build:asan --copt -fsanitize=address +build:asan --copt -O0 +build:asan --copt -fno-omit-frame-pointer +build:asan --copt -g +build:asan --linkopt -fsanitize=address diff --git a/README.md b/README.md index 09cfffa4..316527a5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,12 @@ offer the possibility for metrics to collected by Prometheus, but other push/pull collections can be added as plugins. ## Project Status -Pre-alpha +Alpha + +* parts of the library are instrumented by itself (bytes scraped, number of scrapes) +* there is a working [example](tests/sample_server.cc) that prometheus successfully scrapes +* gauge and counter metrics are implemented, histograms and summaries aren't +* thread safety is missing in registries and metric families (you'd have to lock access yourself for now) ## License MIT diff --git a/lib/BUILD b/lib/BUILD index d5e389b1..1c1691fa 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -2,16 +2,19 @@ cc_library( name = "prometheus-cpp", srcs = ["counter.cc", "gauge.cc", - "exposer.cc"], + "exposer.cc", + "registry.cc"], hdrs = ["counter.h", "gauge.h", "exposer.h", "metric.h", "collectable.h", - "family.h"], + "family.h", + "registry.h"], visibility = ["//visibility:public"], deps = ["@protobuf//:protobuf", "@prometheus_client_model//:prometheus_client_model", "@civetweb//:civetweb", ], + linkstatic = 1, ) diff --git a/lib/exposer.cc b/lib/exposer.cc index 4300c20e..397dfcdd 100644 --- a/lib/exposer.cc +++ b/lib/exposer.cc @@ -1,61 +1,81 @@ #include +#include #include #include -#include -#include #include +#include #include "exposer.h" #include "cpp/metrics.pb.h" namespace prometheus { +MetricsHandler::MetricsHandler( + const std::vector>& collectables, + Registry& registry) + : collectables_(collectables), + bytesTransferedFamily_(registry.add_counter( + "exposer_bytes_transfered", "bytesTransferred to metrics services", + {{"component", "exposer"}})), + bytesTransfered_(bytesTransferedFamily_->add({})), + numScrapesFamily_(registry.add_counter( + "exposer_total_scrapes", "Number of times metrics were scraped", + {{"component", "exposer"}})), + numScrapes_(numScrapesFamily_->add({})) {} + +bool MetricsHandler::handleGet(CivetServer* server, + struct mg_connection* conn) { + using namespace io::prometheus::client; + + std::ostringstream ss; + for (auto&& wcollectable : collectables_) { + auto collectable = wcollectable.lock(); + if (!collectable) { + continue; + } -class MetricsHandler : public CivetHandler { - public: - bool handleGet(CivetServer* server, struct mg_connection* conn) { - using namespace io::prometheus::client; - - MetricFamily message; - message.set_name("Foo"); - message.set_help("Foo help"); - message.set_type(MetricType::COUNTER); - auto metric1 = message.add_metric(); - auto counter = metric1->mutable_counter(); - counter->set_value(1337.0); - - std::ostringstream ss; - { + for (auto&& metricFamily : collectable->collect()) { + { google::protobuf::io::OstreamOutputStream rawOutput{&ss}; google::protobuf::io::CodedOutputStream output(&rawOutput); - // Write the size. - const int size = message.ByteSize(); + const int size = metricFamily.ByteSize(); output.WriteVarint32(size); - } + } - auto buf = ss.str(); - message.AppendToString(&buf); - mg_printf(conn, - "HTTP/1.1 200 OK\r\n" - "Content-Type: " - "application/vnd.google.protobuf; " - "proto=io.prometheus.client.MetricFamily; " - "encoding=delimited\r\n" - "Content-Length: "); - mg_printf(conn, "%lu\r\n\r\n", buf.size()); - mg_write(conn, buf.data(), buf.size()); - return true; + auto buffer = std::string{}; + metricFamily.SerializeToString(&buffer); + ss << buffer; + } } -}; + + auto body = ss.str(); + mg_printf(conn, + "HTTP/1.1 200 OK\r\n" + "Content-Type: " + "application/vnd.google.protobuf; " + "proto=io.prometheus.client.MetricFamily; " + "encoding=delimited\r\n" + "Content-Length: "); + mg_printf(conn, "%lu\r\n\r\n", body.size()); + mg_write(conn, body.data(), body.size()); + bytesTransfered_->inc(body.size()); + numScrapes_->inc(); + return true; +} Exposer::Exposer(std::uint16_t port) - : server_({"listening_ports", std::to_string(port)}) { - MetricsHandler handler; - server_.addHandler("/metrics", &handler); - std::this_thread::sleep_for(std::chrono::seconds(60000)); + : server_({"listening_ports", std::to_string(port)}), + exposerRegistry_( + std::make_shared(std::map{})), + metricsHandler_(collectables_, *exposerRegistry_) { + registerCollectable(exposerRegistry_); + server_.addHandler("/metrics", &metricsHandler_); } -void Exposer::run() {} +void Exposer::registerCollectable( + const std::weak_ptr& collectable) { + collectables_.push_back(collectable); +} } diff --git a/lib/exposer.h b/lib/exposer.h index 1fb88246..534c4f8b 100644 --- a/lib/exposer.h +++ b/lib/exposer.h @@ -1,17 +1,37 @@ #pragma once +#include #include +#include #include "CivetServer.h" +#include "registry.h" namespace prometheus { -class Exposer { +class MetricsHandler : public CivetHandler { public: - Exposer(std::uint16_t port); - void run(); - private: - CivetServer server_; + MetricsHandler(const std::vector>& collectables, + Registry& registry); + + bool handleGet(CivetServer* server, struct mg_connection* conn); + + const std::vector>& collectables_; + Family* bytesTransferedFamily_; + Counter* bytesTransfered_; + Family* numScrapesFamily_; + Counter* numScrapes_; }; +class Exposer { + public: + Exposer(std::uint16_t port); + void registerCollectable(const std::weak_ptr& collectable); + + private: + CivetServer server_; + std::vector> collectables_; + std::shared_ptr exposerRegistry_; + MetricsHandler metricsHandler_; +}; } diff --git a/lib/family.h b/lib/family.h index 7f5890aa..546395ec 100644 --- a/lib/family.h +++ b/lib/family.h @@ -7,7 +7,9 @@ #include #include +#include "counter.h" #include "collectable.h" +#include "gauge.h" #include "metric.h" namespace prometheus { diff --git a/lib/gauge.cc b/lib/gauge.cc index 0ecbdbb7..f142f23e 100644 --- a/lib/gauge.cc +++ b/lib/gauge.cc @@ -27,9 +27,9 @@ void Gauge::dec(double value) { void Gauge::set(double value) { value_.store(value); } void Gauge::change(double value) { - auto current = value_.load(); - while (!value_.compare_exchange_weak(current, current + value)) - ; + auto current = value_.load(); + while (!value_.compare_exchange_weak(current, current + value)) + ; } void Gauge::set_to_current_time() { @@ -38,4 +38,11 @@ void Gauge::set_to_current_time() { } double Gauge::value() const { return value_; } + +io::prometheus::client::Metric Gauge::collect() { + io::prometheus::client::Metric metric; + auto gauge = metric.mutable_gauge(); + gauge->set_value(value()); + return metric; +} } diff --git a/lib/gauge.h b/lib/gauge.h index 664c3cd9..1fc4d7a2 100644 --- a/lib/gauge.h +++ b/lib/gauge.h @@ -2,10 +2,16 @@ #include +#include "cpp/metrics.pb.h" +#include "metric.h" + namespace prometheus { -class Gauge { +class Gauge : public Metric { public: + static const io::prometheus::client::MetricType metric_type = + io::prometheus::client::GAUGE; + Gauge(); Gauge(double); void inc(); @@ -16,6 +22,8 @@ class Gauge { void set_to_current_time(); double value() const; + io::prometheus::client::Metric collect(); + private: void change(double); std::atomic value_; diff --git a/lib/registry.cc b/lib/registry.cc new file mode 100644 index 00000000..a4e6c4ef --- /dev/null +++ b/lib/registry.cc @@ -0,0 +1,43 @@ +#include "registry.h" + +namespace prometheus { + +Registry::Registry(const std::map& constLabels) + : constLabels_(constLabels) {} + +Family* Registry::add_counter( + const std::string& name, const std::string& help, + const std::map& labels) { + auto counterFamily = new Family(name, help, labels); + collectables_.push_back(std::unique_ptr{counterFamily}); + return counterFamily; +} + +Family* Registry::add_gauge( + const std::string& name, const std::string& help, + const std::map& labels) { + auto gaugeFamily = new Family(name, help, labels); + collectables_.push_back(std::unique_ptr{gaugeFamily}); + return gaugeFamily; +} + +std::vector Registry::collect() { + auto results = std::vector{}; + for(auto&& collectable : collectables_) { + auto metrics = collectable->collect(); + results.insert(results.end(), metrics.begin(), metrics.end()); + } + + for (auto&& metricFamily : results) { + for (auto&& metric : *metricFamily.mutable_metric()) { + for (auto&& constLabelPair : constLabels_) { + auto label = metric.add_label(); + label->set_name(constLabelPair.first); + label->set_value(constLabelPair.second); + } + } + } + + return results; +} +} diff --git a/lib/registry.h b/lib/registry.h new file mode 100644 index 00000000..55a94226 --- /dev/null +++ b/lib/registry.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "collectable.h" +#include "cpp/metrics.pb.h" +#include "family.h" + +namespace prometheus { + +class Counter; +class Gauge; + +class Registry : public Collectable { + public: + Registry() = default; + Registry(const std::map& constLabels); + Family* add_counter( + const std::string& name, const std::string& help, + const std::map& labels); + Family* add_gauge(const std::string& name, const std::string& help, + const std::map& labels); + + // collectable + std::vector collect() override; + + private: + std::vector> collectables_; + std::map constLabels_; +}; +} diff --git a/tests/BUILD b/tests/BUILD index 71d7efc7..7cbed192 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -1,9 +1,10 @@ cc_test( name = "prometheus_test", - srcs = ["counter_test.cc", "gauge_test.cc", "mock_metric.h", "family_test.cc"], + srcs = ["counter_test.cc", "gauge_test.cc", "mock_metric.h", "family_test.cc", "registry_test.cc"], copts = ["-Iexternal/googletest/include"], deps = ["@googletest//:main", "//lib:prometheus-cpp"], + linkstatic = 1, ) cc_binary( diff --git a/tests/registry_test.cc b/tests/registry_test.cc new file mode 100644 index 00000000..f37a330a --- /dev/null +++ b/tests/registry_test.cc @@ -0,0 +1,32 @@ +#include + +#include + +#include "lib/registry.h" +#include "lib/collectable.h" + +using namespace testing; +using namespace prometheus; + +class MockCollectable : public Collectable { + public: + MOCK_METHOD0(collect, std::vector()); +}; + +class RegistryTest : public Test {}; + +TEST_F(RegistryTest, collectsSingleMetricFamily) { + auto registry = Registry{}; + auto counterFamily = registry.add_counter("test", "a test", {}); + counterFamily->add({{"name", "counter1"}}); + counterFamily->add({{"name", "counter2"}}); + auto collected = registry.collect(); + ASSERT_EQ(collected.size(), 1); + EXPECT_EQ(collected[0].name(), "test"); + EXPECT_EQ(collected[0].help(), "a test"); + ASSERT_EQ(collected[0].metric_size(), 2); + ASSERT_EQ(collected[0].metric(0).label_size(), 1); + EXPECT_EQ(collected[0].metric(0).label(0).name(), "name"); + ASSERT_EQ(collected[0].metric(1).label_size(), 1); + EXPECT_EQ(collected[0].metric(1).label(0).name(), "name"); +} diff --git a/tests/sample_server.cc b/tests/sample_server.cc index 603114e8..74371166 100644 --- a/tests/sample_server.cc +++ b/tests/sample_server.cc @@ -1,9 +1,24 @@ -#include "lib/exposer.h" +#include +#include +#include +#include +#include -using namespace prometheus; +#include "lib/exposer.h" +#include "lib/registry.h" int main(int argc, char** argv) { - auto server = Exposer{8080}; - server.run(); - return 0; + using namespace prometheus; + + auto exposer = Exposer{8080}; + auto registry = std::make_shared(std::map{{"component", "main"}}); + auto counterFamily = registry->add_counter( + "time_running_seconds", "How many seconds is this server running?", {}); + auto secondCounter = counterFamily->add({}); + exposer.registerCollectable(registry); + for (;;) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + secondCounter->inc(); + } + return 0; }