Boost.Beast Metrics
Boost.Beast is a high performance low level library for creating web services. Here we describe how we integrated Prometheus metrics into our Beast powered services.
In a nutshell, we used the core library from the prometheus-cpp project, and adapted the metrics endpoint to use Beast instead of civet. The push part can also be implemented in terms of Beast, but we have not needed to use the prometheus push gateway yet.
Steps
- Download the source bundle from the prometheus-cpp project, and copy the code (headers and implementation) files from the
core
directory into your project. - Use the code in the
pull
directory as reference, and implement the Beast endpoint that serves the collected metrics. - Instrument the other Beast services as appropriate.
RegistryManager
A global registry for all metrics captured by the system.
// registry.h #pragma once #include "prometheus/registry.h" #include "util/config.h" namespace spt::service { struct RegistryManager { static RegistryManager& getInstance() { static RegistryManager instance; return instance; } static prometheus::Summary::Quantiles quantiles() { return {{0.5, 0.05}, {0.9, 0.01}, {0.99, 0.001}}; } RegistryManager(const RegistryManager&) = delete; RegistryManager(RegistryManager&&) = delete; RegistryManager& operator=(const RegistryManager&) = delete; RegistryManager& operator=(RegistryManager&&) = delete; prometheus::Registry registry; prometheus::Family<prometheus::Summary>* summaries = nullptr; prometheus::Family<prometheus::Counter>* counters = nullptr; private: RegistryManager() { const auto env = environment(); summaries = &prometheus::BuildSummary(). Name("iot_service_sensor_data_time"). Help("How long it took to process the request in milliseconds"). Labels({{"environment", env}, {"system", "iot"}, {"subsystem", "ingress"}}). Register(registry); counters = &prometheus::BuildCounter(). Name("iot_service_sensor_data_requests"). Help("How many requests the service received"). Labels({{"environment", env}, {"system", "iot"}, {"subsystem", "ingress"}}). Register(registry); } std::string environment() { auto& conf = util::Configuration::getInstance(); auto opt = conf.get("/env"); if (opt.has_value()) return opt.value(); else { LOG_WARN << "No configuration for key: /env"; } return std::string{"unknown"}; } ~RegistryManager() = default; }; }
Metrics Endpoint
Prometheus polls a user specified endpoint for the metrics the application wishes to publish. The default target is /metrics, and we have also chosen to bind the metrics to the same resource. The following function publishes the collected metrics when Prometheus polls the endpoint.
// metrics.h #pragma once #include "error.h" #include "registry.h" #include "log/NanoLog.h" #include "prometheus/text_serializer.h" namespace spt::service { template<typename Body, typename Allocator, typename Send> void metrics( http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send) { const auto path = std::string(req.target().data(), req.target().size()); RegistryManager::getInstance().counters->Add( {{"component", "monitoring"}, {"path", path}}).Increment(); const auto data = prometheus::TextSerializer().Serialize( RegistryManager::getInstance().registry.Collect()); http::response<http::dynamic_body> res{http::status::ok, req.version()}; res.set(http::field::server, BOOST_BEAST_VERSION_STRING); res.set(http::field::content_type, "text/plain"); beast::ostream(res.body()) << data; res.prepare_payload(); return send(std::move(res)); } }
Integration Test
A very rudimentary integration test for the metrics endpoint.
// metrics.cpp #include "catch.hpp" #include <boost/beast/core.hpp> #include <boost/beast/http.hpp> #include <boost/beast/version.hpp> #include <boost/asio/connect.hpp> #include <boost/asio/ip/tcp.hpp> #include <boost/beast/http/field.hpp> namespace spt::integration { void metrics(const std::string& token = {}) { namespace beast = boost::beast; // from <boost/beast.hpp> namespace http = beast::http; // from <boost/beast/http.hpp> namespace net = boost::asio; // from <boost/asio.hpp> using tcp = net::ip::tcp; // from <boost/asio/ip/tcp.hpp> net::io_context ioc; tcp::resolver resolver(ioc); beast::tcp_stream stream(ioc); auto const results = resolver.resolve("localhost", "9000"); stream.connect(results); http::request<http::string_body> req{http::verb::get, "/metrics", 11}; req.set(http::field::host, "localhost"); req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); if (!token.empty()) req.set("Authorization", "Bearer " + token); http::write(stream, req); beast::flat_buffer buffer; http::response<http::string_body> res; http::read(stream, buffer, res); REQUIRE(res.body().size() > 0); const auto ct = res[http::field::content_type]; REQUIRE_FALSE(ct.empty()); REQUIRE(ct == "text/plain"); beast::error_code ec; stream.socket().shutdown(tcp::socket::shutdown_both, ec); if (ec && ec != beast::errc::not_connected) throw beast::system_error{ec}; } } SCENARIO( "Service metrics endpoint", "[status]" ) { GIVEN("IoT receiver service is running") { THEN("Can access metrics endpoint without authentication") { REQUIRE_NOTHROW(spt::integration::metrics()); } AND_THEN("Can access metrics endpoint with authentication") { REQUIRE_NOTHROW(spt::integration::metrics("<complex valid token>")); } AND_THEN("Can access metrics endpoint with invalid authentication") { REQUIRE_NOTHROW(spt::integration::metrics("abc123")); } } }