// Prometheus exporter test. Spins up the registry + HTTP server on a // loopback port, scrapes via raw TCP, and asserts the text-exposition // format renders correctly with counters, gauges, labels, and HELP/TYPE // metadata. #include #include #include #include #include #include "secsgem/metrics/prometheus.hpp" using namespace secsgem::metrics; using namespace std::chrono_literals; namespace { // Fire one HTTP GET / against the server and return the body. std::string scrape(asio::io_context& io, uint16_t port) { asio::ip::tcp::socket sock(io); asio::error_code ec; sock.connect({asio::ip::address_v4::loopback(), port}, ec); REQUIRE_FALSE(ec); const std::string req = "GET /metrics HTTP/1.1\r\nHost: localhost\r\n\r\n"; asio::write(sock, asio::buffer(req), ec); REQUIRE_FALSE(ec); asio::streambuf buf; asio::read(sock, buf, ec); // EOF is expected (server closes after reply). REQUIRE((ec == asio::error::eof || !ec)); std::istream is(&buf); std::string line, headers, body; bool in_body = false; while (std::getline(is, line)) { if (in_body) body += line + "\n"; else if (line == "\r" || line.empty()) in_body = true; else headers += line + "\n"; } return body; } } // namespace TEST_CASE("Prometheus: render counters and gauges") { Registry r; r.describe("secsgem_messages_total", "messages dispatched", MetricType::Counter); r.inc("secsgem_messages_total", {{"dir", "rx"}, {"stream", "1"}}, 3); r.inc("secsgem_messages_total", {{"dir", "tx"}, {"stream", "1"}}, 5); r.set_gauge("secsgem_spool_depth", 42); const auto text = r.render(); CHECK(text.find("# HELP secsgem_messages_total messages dispatched") != std::string::npos); CHECK(text.find("# TYPE secsgem_messages_total counter") != std::string::npos); CHECK(text.find("secsgem_messages_total{dir=\"rx\",stream=\"1\"} 3") != std::string::npos); CHECK(text.find("secsgem_messages_total{dir=\"tx\",stream=\"1\"} 5") != std::string::npos); CHECK(text.find("# TYPE secsgem_spool_depth gauge") != std::string::npos); CHECK(text.find("secsgem_spool_depth 42") != std::string::npos); } TEST_CASE("Prometheus: HTTP server serves /metrics") { asio::io_context server_io; auto reg = std::make_shared(); reg->describe("secsgem_test_total", "test counter", MetricType::Counter); reg->inc("secsgem_test_total", {{"path", "happy"}}, 7); reg->set_gauge("secsgem_test_gauge", 3.14); // Port 0 → OS picks a free one. auto server = std::make_shared(server_io, 0, reg); server->start(); const auto port = server->port(); std::thread server_thread([&] { server_io.run(); }); // Give the acceptor a tick to arm. std::this_thread::sleep_for(50ms); asio::io_context client_io; const auto body = scrape(client_io, port); CHECK(body.find("secsgem_test_total{path=\"happy\"} 7") != std::string::npos); CHECK(body.find("secsgem_test_gauge 3.14") != std::string::npos); server_io.stop(); server_thread.join(); } TEST_CASE("Prometheus: live updates picked up on next scrape") { asio::io_context server_io; auto reg = std::make_shared(); auto server = std::make_shared(server_io, 0, reg); server->start(); const auto port = server->port(); std::thread server_thread([&] { server_io.run(); }); std::this_thread::sleep_for(50ms); reg->inc("secsgem_dynamic_total", {{"k", "v"}}); asio::io_context c1; auto body1 = scrape(c1, port); CHECK(body1.find("secsgem_dynamic_total{k=\"v\"} 1") != std::string::npos); reg->inc("secsgem_dynamic_total", {{"k", "v"}}, 4); asio::io_context c2; auto body2 = scrape(c2, port); CHECK(body2.find("secsgem_dynamic_total{k=\"v\"} 5") != std::string::npos); server_io.stop(); server_thread.join(); }