feat: Healthcheck endpoint (#1751)

Fixes #1759
This commit is contained in:
Alex Kremer
2024-11-26 13:56:25 +00:00
committed by GitHub
parent 541bf4395f
commit fd73b90416
6 changed files with 82 additions and 19 deletions

View File

@@ -65,6 +65,14 @@ namespace app {
namespace { namespace {
auto constexpr HealthCheckHTML = R"html(
<!DOCTYPE html>
<html>
<head><title>Test page for Clio</title></head>
<body><h1>Clio Test</h1><p>This page shows Clio http(s) connectivity is working.</p></body>
</html>
)html";
/** /**
* @brief Start context threads * @brief Start context threads
* *
@@ -178,6 +186,16 @@ ClioApplication::run(bool const useNgWebServer)
} }
); );
httpServer->onGet(
"/health",
[](web::ng::Request const& request,
web::ng::ConnectionMetadata&,
web::SubscriptionContextPtr,
boost::asio::yield_context) -> web::ng::Response {
return web::ng::Response{boost::beast::http::status::ok, HealthCheckHTML, request};
}
);
util::Logger webServerLog{"WebServer"}; util::Logger webServerLog{"WebServer"};
auto onRequest = [adminVerifier, &webServerLog, &handler]( auto onRequest = [adminVerifier, &webServerLog, &handler](
web::ng::Request const& request, web::ng::Request const& request,

View File

@@ -36,7 +36,6 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <exception> #include <exception>
#include <iostream>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -49,10 +48,8 @@ resolve(std::string const& ip, std::string const& port)
{ {
web::Resolver resolver; web::Resolver resolver;
if (auto const results = resolver.resolve(ip, port); not results.empty()) { if (auto const results = resolver.resolve(ip, port); not results.empty())
std::cout << "resolved ip: '" << results.at(0) << '\n';
return results.at(0); return results.at(0);
}
throw std::runtime_error("Failed to resolve " + ip + ":" + port); throw std::runtime_error("Failed to resolve " + ip + ":" + port);
} }

View File

@@ -62,6 +62,14 @@
namespace web::impl { namespace web::impl {
static auto constexpr HealthCheckHTML = R"html(
<!DOCTYPE html>
<html>
<head><title>Test page for Clio</title></head>
<body><h1>Clio Test</h1><p>This page shows Clio http(s) connectivity is working.</p></body>
</html>
)html";
using tcp = boost::asio::ip::tcp; using tcp = boost::asio::ip::tcp;
/** /**
@@ -207,6 +215,9 @@ public:
if (ec) if (ec)
return httpFail(ec, "read"); return httpFail(ec, "read");
if (req_.method() == http::verb::get and req_.target() == "/health")
return sender_(httpResponse(http::status::ok, "text/html", HealthCheckHTML));
// Update isAdmin property of the connection // Update isAdmin property of the connection
ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp); ConnectionBase::isAdmin_ = adminVerification_->isAdmin(req_, this->clientIp);

View File

@@ -36,6 +36,7 @@
#include <boost/beast/http.hpp> // IWYU pragma: keep #include <boost/beast/http.hpp> // IWYU pragma: keep
#include <boost/beast/http/field.hpp> #include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp> #include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp> #include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/verb.hpp> #include <boost/beast/http/verb.hpp>
#include <boost/beast/http/write.hpp> // IWYU pragma: keep #include <boost/beast/http/write.hpp> // IWYU pragma: keep
@@ -58,7 +59,7 @@ using tcp = boost::asio::ip::tcp;
namespace { namespace {
std::string std::pair<boost::beast::http::status, std::string>
syncRequest( syncRequest(
std::string const& host, std::string const& host,
std::string const& port, std::string const& port,
@@ -96,7 +97,7 @@ syncRequest(
boost::beast::error_code ec; boost::beast::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_both, ec); stream.socket().shutdown(tcp::socket::shutdown_both, ec);
return res.body(); return {res.result(), res.body()};
} }
} // namespace } // namespace
@@ -105,7 +106,7 @@ WebHeader::WebHeader(http::field name, std::string value) : name(name), value(st
{ {
} }
std::string std::pair<boost::beast::http::status, std::string>
HttpSyncClient::post( HttpSyncClient::post(
std::string const& host, std::string const& host,
std::string const& port, std::string const& port,
@@ -116,7 +117,7 @@ HttpSyncClient::post(
return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post); return syncRequest(host, port, body, std::move(additionalHeaders), http::verb::post);
} }
std::string std::pair<boost::beast::http::status, std::string>
HttpSyncClient::get( HttpSyncClient::get(
std::string const& host, std::string const& host,
std::string const& port, std::string const& port,

View File

@@ -26,6 +26,7 @@
#include <boost/beast/core/tcp_stream.hpp> #include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http/field.hpp> #include <boost/beast/http/field.hpp>
#include <boost/beast/http/message.hpp> #include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp> #include <boost/beast/http/string_body.hpp>
#include <chrono> #include <chrono>
@@ -33,6 +34,7 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <utility>
#include <vector> #include <vector>
struct WebHeader { struct WebHeader {
@@ -43,7 +45,7 @@ struct WebHeader {
}; };
struct HttpSyncClient { struct HttpSyncClient {
static std::string static std::pair<boost::beast::http::status, std::string>
post( post(
std::string const& host, std::string const& host,
std::string const& port, std::string const& port,
@@ -51,7 +53,7 @@ struct HttpSyncClient {
std::vector<WebHeader> additionalHeaders = {} std::vector<WebHeader> additionalHeaders = {}
); );
static std::string static std::pair<boost::beast::http::status, std::string>
get(std::string const& host, get(std::string const& host,
std::string const& port, std::string const& port,
std::string const& body, std::string const& body,

View File

@@ -56,6 +56,7 @@
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <thread> #include <thread>
#include <tuple>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -214,8 +215,9 @@ TEST_F(WebServerTest, Http)
{ {
auto e = std::make_shared<EchoExecutor>(); auto e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e); auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const res = HttpSyncClient::post("localhost", port, R"({"Hello":1})"); auto const [status, res] = HttpSyncClient::post("localhost", port, R"({"Hello":1})");
EXPECT_EQ(res, R"({"Hello":1})"); EXPECT_EQ(res, R"({"Hello":1})");
EXPECT_EQ(status, boost::beast::http::status::ok);
} }
TEST_F(WebServerTest, Ws) TEST_F(WebServerTest, Ws)
@@ -233,11 +235,12 @@ TEST_F(WebServerTest, HttpInternalError)
{ {
auto e = std::make_shared<ExceptionExecutor>(); auto e = std::make_shared<ExceptionExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuard, e); auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const res = HttpSyncClient::post("localhost", port, R"({})"); auto const [status, res] = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ( EXPECT_EQ(
res, res,
R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})" R"({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})"
); );
EXPECT_EQ(status, boost::beast::http::status::internal_server_error);
} }
TEST_F(WebServerTest, WsInternalError) TEST_F(WebServerTest, WsInternalError)
@@ -316,13 +319,16 @@ TEST_F(WebServerTest, HttpRequestOverload)
{ {
auto e = std::make_shared<EchoExecutor>(); auto e = std::make_shared<EchoExecutor>();
auto const server = makeServerSync(cfg, ctx, dosGuardOverload, e); auto const server = makeServerSync(cfg, ctx, dosGuardOverload, e);
auto res = HttpSyncClient::post("localhost", port, R"({})"); auto [status, res] = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ(res, "{}"); EXPECT_EQ(res, "{}");
res = HttpSyncClient::post("localhost", port, R"({})"); EXPECT_EQ(status, boost::beast::http::status::ok);
std::tie(status, res) = HttpSyncClient::post("localhost", port, R"({})");
EXPECT_EQ( EXPECT_EQ(
res, res,
R"({"error":"slowDown","error_code":10,"error_message":"You are placing too much load on the server.","status":"error","type":"response"})" R"({"error":"slowDown","error_code":10,"error_message":"You are placing too much load on the server.","status":"error","type":"response"})"
); );
EXPECT_EQ(status, boost::beast::http::status::service_unavailable);
} }
TEST_F(WebServerTest, WsRequestOverload) TEST_F(WebServerTest, WsRequestOverload)
@@ -349,11 +355,12 @@ TEST_F(WebServerTest, HttpPayloadOverload)
std::string const s100(100, 'a'); std::string const s100(100, 'a');
auto e = std::make_shared<EchoExecutor>(); auto e = std::make_shared<EchoExecutor>();
auto server = makeServerSync(cfg, ctx, dosGuardOverload, e); auto server = makeServerSync(cfg, ctx, dosGuardOverload, e);
auto const res = HttpSyncClient::post("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100)); auto const [status, res] = HttpSyncClient::post("localhost", port, fmt::format(R"({{"payload":"{}"}})", s100));
EXPECT_EQ( EXPECT_EQ(
res, res,
R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})" R"({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})"
); );
EXPECT_EQ(status, boost::beast::http::status::ok);
} }
TEST_F(WebServerTest, WsPayloadOverload) TEST_F(WebServerTest, WsPayloadOverload)
@@ -393,6 +400,26 @@ TEST_F(WebServerTest, WsTooManyConnection)
EXPECT_TRUE(exceptionThrown); EXPECT_TRUE(exceptionThrown);
} }
TEST_F(WebServerTest, HealthCheck)
{
auto e = std::make_shared<ExceptionExecutor>(); // request handled before we get to executor
auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/health");
EXPECT_FALSE(res.empty());
EXPECT_EQ(status, boost::beast::http::status::ok);
}
TEST_F(WebServerTest, GetOtherThanHealthCheck)
{
auto e = std::make_shared<ExceptionExecutor>(); // request handled before we get to executor
auto const server = makeServerSync(cfg, ctx, dosGuard, e);
auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/");
EXPECT_FALSE(res.empty());
EXPECT_EQ(status, boost::beast::http::status::bad_request);
}
std::string std::string
JSONServerConfigWithAdminPassword(uint32_t const port) JSONServerConfigWithAdminPassword(uint32_t const port)
{ {
@@ -500,8 +527,11 @@ TEST_P(WebServerAdminTest, HttpAdminCheck)
auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e); auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e);
std::string const request = "Why hello"; std::string const request = "Why hello";
uint32_t const webServerPort = serverConfig.value<uint32_t>("server.port"); uint32_t const webServerPort = serverConfig.value<uint32_t>("server.port");
auto const res = HttpSyncClient::post("localhost", std::to_string(webServerPort), request, GetParam().headers); auto const [status, res] =
HttpSyncClient::post("localhost", std::to_string(webServerPort), request, GetParam().headers);
EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse)); EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse));
EXPECT_EQ(status, boost::beast::http::status::ok);
} }
INSTANTIATE_TEST_CASE_P( INSTANTIATE_TEST_CASE_P(
@@ -618,8 +648,10 @@ TEST_F(WebServerPrometheusTest, rejectedWithoutAdminPassword)
uint32_t const webServerPort = tests::util::generateFreePort(); uint32_t const webServerPort = tests::util::generateFreePort();
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))}; Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e); auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::get("localhost", std::to_string(webServerPort), "", "/metrics"); auto const [status, res] = HttpSyncClient::get("localhost", std::to_string(webServerPort), "", "/metrics");
EXPECT_EQ(res, "Only admin is allowed to collect metrics"); EXPECT_EQ(res, "Only admin is allowed to collect metrics");
EXPECT_EQ(status, boost::beast::http::status::unauthorized);
} }
TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled) TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled)
@@ -641,7 +673,7 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled)
Config const serverConfig{boost::json::parse(JSONServerConfigWithDisabledPrometheus)}; Config const serverConfig{boost::json::parse(JSONServerConfigWithDisabledPrometheus)};
PrometheusService::init(serverConfig); PrometheusService::init(serverConfig);
auto server = makeServerSync(serverConfig, ctx, dosGuard, e); auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::get( auto const [status, res] = HttpSyncClient::get(
"localhost", "localhost",
std::to_string(webServerPort), std::to_string(webServerPort),
"", "",
@@ -652,6 +684,7 @@ TEST_F(WebServerPrometheusTest, rejectedIfPrometheusIsDisabled)
)} )}
); );
EXPECT_EQ(res, "Prometheus is disabled in clio config"); EXPECT_EQ(res, "Prometheus is disabled in clio config");
EXPECT_EQ(status, boost::beast::http::status::forbidden);
} }
TEST_F(WebServerPrometheusTest, validResponse) TEST_F(WebServerPrometheusTest, validResponse)
@@ -662,7 +695,7 @@ TEST_F(WebServerPrometheusTest, validResponse)
auto e = std::make_shared<EchoExecutor>(); auto e = std::make_shared<EchoExecutor>();
Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))}; Config const serverConfig{boost::json::parse(JSONServerConfigWithAdminPassword(webServerPort))};
auto server = makeServerSync(serverConfig, ctx, dosGuard, e); auto server = makeServerSync(serverConfig, ctx, dosGuard, e);
auto const res = HttpSyncClient::get( auto const [status, res] = HttpSyncClient::get(
"localhost", "localhost",
std::to_string(webServerPort), std::to_string(webServerPort),
"", "",
@@ -673,4 +706,5 @@ TEST_F(WebServerPrometheusTest, validResponse)
)} )}
); );
EXPECT_EQ(res, "# TYPE test_counter counter\ntest_counter 1\n\n"); EXPECT_EQ(res, "# TYPE test_counter counter\ntest_counter 1\n\n");
EXPECT_EQ(status, boost::beast::http::status::ok);
} }