Changes
diff --git a/config.example.yaml b/config.example.yaml
index a3f2ca4..9bb112e 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -20,6 +20,8 @@ groups:
unit: minute
endpoint:
protocol: HTTP
+ # If set, only this exact response code is considered good.
+ # expected_code: 200
# If omitted, the service-level URL is used for HTTP probes.
# url: https://blog.example/
timeout_second: 5
diff --git a/prd.md b/prd.md
index 0dedc67..856a17c 100644
--- a/prd.md
+++ b/prd.md
@@ -15,7 +15,8 @@ A simple self-hosted service status tracker.
- It monitors a bunch of endpoints which are defined in the
config file
- Endpoints may be monitored by one of the following protocols:
- * HTTP (check it returns 2xx or 3xx status)
+ * HTTP (check it returns 2xx or 3xx status, unless an exact
+ `expected_code` is configured)
* TCP or UDP port open
* ICMP
- Possible config snippet for endpoint:
@@ -30,6 +31,8 @@ A simple self-hosted service status tracker.
endpoint:
# Different protocols may have different set of fields.
protocol: HTTP
+ # Optional exact response code required for a good status.
+ # expected_code: 200
# This could be different from the user-facing URL. If this is omitted, use the user-facing URL
url: https://blog.mws.rocks/
timeout_second: 5
diff --git a/src/configuration.cpp b/src/configuration.cpp
index db94928..bdf7d55 100644
--- a/src/configuration.cpp
+++ b/src/configuration.cpp
@@ -191,7 +191,17 @@ mw::E<EndpointConfig> parseEndpoint(Node endpoint, std::string_view context)
{
ASSIGN_OR_RETURN(auto url,
optionalString(endpoint, "url", context));
- return EndpointConfig{HttpEndpoint{url.value_or("")}};
+ ASSIGN_OR_RETURN(auto expected_code,
+ optionalInteger<unsigned int>(
+ endpoint, "expected_code", context));
+ if(expected_code &&
+ (*expected_code < 100 || *expected_code > 599))
+ {
+ return std::unexpected(configError(
+ context, "expected_code must be between 100 and 599"));
+ }
+ return EndpointConfig{HttpEndpoint{
+ url.value_or(""), std::move(expected_code)}};
}
if(protocol == "tcp")
{
diff --git a/src/probe.cpp b/src/probe.cpp
index 0f6edb2..92de548 100644
--- a/src/probe.cpp
+++ b/src/probe.cpp
@@ -50,6 +50,12 @@ mw::E<void> validate(const HttpEndpoint& config)
{
return std::unexpected(mw::runtimeError("Expected HTTP(S) URL"));
}
+ if(config.expected_code &&
+ (*config.expected_code < 100 || *config.expected_code > 599))
+ {
+ return std::unexpected(mw::runtimeError(
+ "Expected HTTP status code must be between 100 and 599"));
+ }
const auto features = curl_version_info(CURLVERSION_NOW)->features;
if(!(features & CURL_VERSION_ASYNCHDNS))
{
@@ -157,7 +163,10 @@ mw::E<ProbeStatus> check(const HttpEndpoint& config,
{
return ProbeStatus::BAD;
}
- return response->status >= 200 && response->status < 400
+ const auto good = config.expected_code
+ ? response->status == static_cast<int>(*config.expected_code)
+ : response->status >= 200 && response->status < 400;
+ return good
? ProbeStatus::GOOD : ProbeStatus::BAD;
}
diff --git a/src/probe.h b/src/probe.h
index d9a336b..6f94f64 100644
--- a/src/probe.h
+++ b/src/probe.h
@@ -4,16 +4,19 @@
#include <cstdint>
#include <string>
#include <memory>
+#include <optional>
#include <mw/error.hpp>
#include "probe_status.h"
-/// HTTP configuration; direct 2xx and 3xx responses are healthy.
+/// HTTP configuration; direct 2xx and 3xx responses are healthy by default.
struct HttpEndpoint
{
/// HTTP or HTTPS URL, after applying any service URL fallback.
std::string url;
+ /// Optional exact HTTP response code required for a healthy result.
+ std::optional<unsigned int> expected_code;
};
/// TCP connection configuration.
diff --git a/tests/configuration_test.cpp b/tests/configuration_test.cpp
index a52344e..93e40c2 100644
--- a/tests/configuration_test.cpp
+++ b/tests/configuration_test.cpp
@@ -51,6 +51,7 @@ TEST(Configuration, LoadsGroupedProtocolSettings)
" unit: minute\n"
" endpoint:\n"
" protocol: HTTP\n"
+ " expected_code: 204\n"
" ssh:\n"
" name: SSH\n"
" description: SSH server\n"
@@ -98,6 +99,9 @@ TEST(Configuration, LoadsGroupedProtocolSettings)
EXPECT_EQ(services[0].interval, std::chrono::minutes(1));
ASSERT_TRUE(std::holds_alternative<HttpEndpoint>(services[0].endpoint));
EXPECT_TRUE(std::get<HttpEndpoint>(services[0].endpoint).url.empty());
+ ASSERT_TRUE(std::get<HttpEndpoint>(services[0].endpoint).expected_code);
+ EXPECT_EQ(*std::get<HttpEndpoint>(services[0].endpoint).expected_code,
+ 204);
ASSERT_TRUE(services[0].url);
EXPECT_EQ(*services[0].url, "https://blog.example/");
@@ -135,6 +139,18 @@ TEST(Configuration, RejectsInvalidValues)
" services:\n"
" x:\n"
" name: x\n"
+ " interval: {value: 1, unit: minute}\n"
+ " endpoint:\n"
+ " protocol: HTTP\n"
+ " url: http://x\n"
+ " expected_code: 99\n",
+ "worker_count: 1\n"
+ "database_path: db\n"
+ "groups:\n"
+ " - name: g\n"
+ " services:\n"
+ " x:\n"
+ " name: x\n"
" description: x\n"
" interval: {value: 1, unit: minute}\n"
" endpoint: {protocol: TCP, host: x, port: 0}\n",
diff --git a/tests/probe_test.cpp b/tests/probe_test.cpp
index d3076d5..ff914b0 100644
--- a/tests/probe_test.cpp
+++ b/tests/probe_test.cpp
@@ -174,6 +174,8 @@ TEST(Probe, InvalidConfiguration)
EXPECT_FALSE(createProbe(HttpEndpoint{"file:///etc/hosts"}));
EXPECT_FALSE(createProbe(HttpEndpoint{"http://"}));
EXPECT_FALSE(createProbe(HttpEndpoint{"http://localhost"}, 0s));
+ EXPECT_FALSE(createProbe(HttpEndpoint{"http://localhost", 99}));
+ EXPECT_FALSE(createProbe(HttpEndpoint{"http://localhost", 600}));
EXPECT_FALSE(createProbe(TcpEndpoint{"localhost", 80}, -1s));
EXPECT_FALSE(createProbe(TcpEndpoint{
"localhost", 80}, std::chrono::seconds::max()));
@@ -300,6 +302,28 @@ TEST_F(ProbeTest, HttpStatusCodes)
}
}
+TEST_F(ProbeTest, HttpExpectedStatusCode)
+{
+ ASSERT_TRUE(bindServer(SOCK_STREAM, AF_INET, true));
+ worker = std::jthread(&ProbeTest::serveHttp, this, 204, false);
+ auto good_probe = createProbe(HttpEndpoint{
+ "http://127.0.0.1:" + std::to_string(port), 204}, 1s);
+ ASSERT_TRUE(good_probe) << good_probe.error().msg();
+ auto good = (*good_probe)->probe();
+ ASSERT_TRUE(good) << good.error().msg();
+ EXPECT_EQ(good->status, ProbeStatus::GOOD);
+ worker.join();
+
+ worker = std::jthread(&ProbeTest::serveHttp, this, 200, false);
+ auto bad_probe = createProbe(HttpEndpoint{
+ "http://127.0.0.1:" + std::to_string(port), 204}, 1s);
+ ASSERT_TRUE(bad_probe) << bad_probe.error().msg();
+ auto bad = (*bad_probe)->probe();
+ ASSERT_TRUE(bad) << bad.error().msg();
+ EXPECT_EQ(bad->status, ProbeStatus::BAD);
+ worker.join();
+}
+
TEST_F(ProbeTest, HttpTimeout)
{
ASSERT_TRUE(bindServer(SOCK_STREAM, AF_INET, true));