Files
clio/tests/unit/rpc/CountersTests.cpp
2026-03-24 15:25:32 +00:00

348 lines
12 KiB
C++

#include "rpc/Counters.hpp"
#include "rpc/JS.hpp"
#include "rpc/WorkQueue.hpp"
#include "util/MockPrometheus.hpp"
#include "util/prometheus/Counter.hpp"
#include "util/prometheus/Histogram.hpp"
#include <boost/json/object.hpp>
#include <boost/json/value_to.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/protocol/jss.h>
#include <chrono>
#include <cstdint>
#include <string>
using namespace rpc;
using util::prometheus::CounterInt;
using util::prometheus::WithMockPrometheus;
using util::prometheus::WithPrometheus;
struct RPCCountersBaseTest {
struct WorkQueueMock : Reportable {
MOCK_METHOD(boost::json::object, report, (), (const, override));
};
testing::StrictMock<WorkQueueMock> workQueueMock;
};
struct RPCCountersTest : WithPrometheus, RPCCountersBaseTest {
Counters counters{workQueueMock};
};
TEST_F(RPCCountersTest, CheckThatCountersAddUp)
{
for (auto i = 0u; i < 512u; ++i) {
counters.rpcErrored("error");
counters.rpcComplete("complete", std::chrono::milliseconds{1u});
counters.rpcForwarded("forward");
counters.rpcFailedToForward("failedToForward");
counters.rpcFailed("failed");
counters.onTooBusy();
counters.onNotReady();
counters.onBadSyntax();
counters.onUnknownCommand();
counters.onInternalError();
}
EXPECT_CALL(workQueueMock, report);
auto const report = counters.report();
auto const& rpc = report.at(JS(rpc)).as_object();
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("error").as_object().at(JS(started))), "512"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("error").as_object().at(JS(finished))), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("error").as_object().at(JS(errored))), "512"
);
EXPECT_EQ(boost::json::value_to<std::string>(rpc.at("error").as_object().at("forwarded")), "0");
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("error").as_object().at("failed_forward")), "0"
);
EXPECT_EQ(boost::json::value_to<std::string>(rpc.at("error").as_object().at(JS(failed))), "0");
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("complete").as_object().at(JS(started))), "512"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("complete").as_object().at(JS(finished))), "512"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("complete").as_object().at(JS(errored))), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("complete").as_object().at("forwarded")), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("complete").as_object().at("failed_forward")), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("complete").as_object().at(JS(failed))), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("complete").as_object().at(JS(duration_us))),
"512000"
); // 1000 per call
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("forward").as_object().at(JS(started))), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("forward").as_object().at(JS(finished))), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("forward").as_object().at(JS(errored))), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("forward").as_object().at("forwarded")), "512"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("forward").as_object().at("failed_forward")), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("forward").as_object().at(JS(failed))), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failed").as_object().at(JS(started))), "512"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failed").as_object().at(JS(finished))), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failed").as_object().at(JS(errored))), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failed").as_object().at("forwarded")), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failed").as_object().at("failed_forward")), "0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failed").as_object().at(JS(failed))), "512"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failedToForward").as_object().at(JS(started))),
"0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failedToForward").as_object().at(JS(finished))),
"0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failedToForward").as_object().at(JS(errored))),
"0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failedToForward").as_object().at("forwarded")),
"0"
);
EXPECT_EQ(
boost::json::value_to<std::string>(
rpc.at("failedToForward").as_object().at("failed_forward")
),
"512"
);
EXPECT_EQ(
boost::json::value_to<std::string>(rpc.at("failedToForward").as_object().at(JS(failed))),
"0"
);
EXPECT_EQ(boost::json::value_to<std::string>(report.at("too_busy_errors")), "512");
EXPECT_EQ(boost::json::value_to<std::string>(report.at("not_ready_errors")), "512");
EXPECT_EQ(boost::json::value_to<std::string>(report.at("bad_syntax_errors")), "512");
EXPECT_EQ(boost::json::value_to<std::string>(report.at("unknown_command_errors")), "512");
EXPECT_EQ(boost::json::value_to<std::string>(report.at("internal_errors")), "512");
}
struct RPCCountersMockPrometheusTests : WithMockPrometheus, RPCCountersBaseTest {
Counters counters{workQueueMock};
};
TEST_F(RPCCountersMockPrometheusTests, rpcFailed)
{
auto& startedMock =
makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"started\"}");
auto& failedMock =
makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"failed\"}");
EXPECT_CALL(startedMock, add(1));
EXPECT_CALL(failedMock, add(1));
counters.rpcFailed("test");
}
TEST_F(RPCCountersMockPrometheusTests, rpcErrored)
{
auto& startedMock =
makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"started\"}");
auto& erroredMock =
makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"errored\"}");
EXPECT_CALL(startedMock, add(1));
EXPECT_CALL(erroredMock, add(1));
counters.rpcErrored("test");
}
TEST_F(RPCCountersMockPrometheusTests, rpcComplete)
{
auto& startedMock =
makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"started\"}");
auto& finishedMock =
makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"finished\"}");
auto& durationMock = makeMock<CounterInt>("rpc_method_duration_us", "{method=\"test\"}");
EXPECT_CALL(startedMock, add(1));
EXPECT_CALL(finishedMock, add(1));
EXPECT_CALL(durationMock, add(123));
counters.rpcComplete("test", std::chrono::microseconds(123));
}
TEST_F(RPCCountersMockPrometheusTests, rpcForwarded)
{
auto& forwardedMock =
makeMock<CounterInt>("rpc_method_total_number", "{method=\"test\",status=\"forwarded\"}");
EXPECT_CALL(forwardedMock, add(1));
counters.rpcForwarded("test");
}
TEST_F(RPCCountersMockPrometheusTests, rpcFailedToForwarded)
{
auto& failedForwardMock = makeMock<CounterInt>(
"rpc_method_total_number", "{method=\"test\",status=\"failed_forward\"}"
);
EXPECT_CALL(failedForwardMock, add(1));
counters.rpcFailedToForward("test");
}
TEST_F(RPCCountersMockPrometheusTests, onTooBusy)
{
auto& tooBusyMock = makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"too_busy\"}");
EXPECT_CALL(tooBusyMock, add(1));
counters.onTooBusy();
}
TEST_F(RPCCountersMockPrometheusTests, onNotReady)
{
auto& notReadyMock =
makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"not_ready\"}");
EXPECT_CALL(notReadyMock, add(1));
counters.onNotReady();
}
TEST_F(RPCCountersMockPrometheusTests, onBadSyntax)
{
auto& badSyntaxMock =
makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"bad_syntax\"}");
EXPECT_CALL(badSyntaxMock, add(1));
counters.onBadSyntax();
}
TEST_F(RPCCountersMockPrometheusTests, onUnknownCommand)
{
auto& unknownCommandMock =
makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"unknown_command\"}");
EXPECT_CALL(unknownCommandMock, add(1));
counters.onUnknownCommand();
}
TEST_F(RPCCountersMockPrometheusTests, onInternalError)
{
auto& internalErrorMock =
makeMock<CounterInt>("rpc_error_total_number", "{error_type=\"internal_error\"}");
EXPECT_CALL(internalErrorMock, add(1));
counters.onInternalError();
}
struct RPCCountersMockPrometheusRecotdLedgerRequestTest : RPCCountersMockPrometheusTests {
testing::StrictMock<util::prometheus::MockHistogramImpl<int64_t>>& ageLedgersHistogramMock =
makeMock<util::prometheus::HistogramInt>("rpc_requested_ledger_age_histogram", "");
};
TEST_F(RPCCountersMockPrometheusRecotdLedgerRequestTest, currentLedger)
{
// "current" is not tracked in the histogram (it's not a historical ledger lookup)
// No mock expectations needed
boost::json::object params;
params["ledger_index"] = "current";
counters.recordLedgerRequest(params, 1000);
}
TEST_F(RPCCountersMockPrometheusRecotdLedgerRequestTest, validateLedger)
{
EXPECT_CALL(ageLedgersHistogramMock, observe(0));
boost::json::object params;
params["ledger_index"] = "validated";
counters.recordLedgerRequest(params, 1000);
}
TEST_F(RPCCountersMockPrometheusRecotdLedgerRequestTest, validatedDefaultLedger)
{
EXPECT_CALL(ageLedgersHistogramMock, observe(0));
boost::json::object const params;
counters.recordLedgerRequest(params, 1000);
}
TEST_F(RPCCountersMockPrometheusRecotdLedgerRequestTest, specificLedger)
{
auto& ageLedgersHistogramMock =
makeMock<util::prometheus::HistogramInt>("rpc_requested_ledger_age_histogram", "");
EXPECT_CALL(ageLedgersHistogramMock, observe(100)); // age is 1000 - 900 = 100
boost::json::object params;
params["ledger_index"] = 900;
counters.recordLedgerRequest(params, 1000);
}
TEST_F(RPCCountersMockPrometheusRecotdLedgerRequestTest, stringNumberLedger)
{
EXPECT_CALL(ageLedgersHistogramMock, observe(50)); // 1000 - 950 = 50 ledgers
boost::json::object params;
params["ledger_index"] = "950";
counters.recordLedgerRequest(params, 1000);
}
TEST_F(RPCCountersMockPrometheusRecotdLedgerRequestTest, zeroAgeLedger)
{
auto& ageLedgersHistogramMock =
makeMock<util::prometheus::HistogramInt>("rpc_requested_ledger_age_histogram", "");
EXPECT_CALL(ageLedgersHistogramMock, observe(0)); // 1000 - 1000 = 0 ledgers
boost::json::object params;
params["ledger_index"] = 1000; // Same as current
counters.recordLedgerRequest(params, 1000);
}
TEST_F(RPCCountersMockPrometheusRecotdLedgerRequestTest, ledgerHashRequest)
{
auto& ledgerHashCounterMock = makeMock<CounterInt>("rpc_ledger_hash_requests_total_number", "");
EXPECT_CALL(ledgerHashCounterMock, add(1));
boost::json::object params;
params["ledger_hash"] = "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789";
counters.recordLedgerRequest(params, 1000);
}
TEST_F(RPCCountersMockPrometheusRecotdLedgerRequestTest, ledgerHashWithIndexIgnoresIndex)
{
auto& ledgerHashCounterMock = makeMock<CounterInt>("rpc_ledger_hash_requests_total_number", "");
// When both ledger_hash and ledger_index are present, only ledger_hash counter should be
// incremented
EXPECT_CALL(ledgerHashCounterMock, add(1));
// No histogram call should be made
boost::json::object params;
params["ledger_hash"] = "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789";
params["ledger_index"] = 900; // This should be ignored
counters.recordLedgerRequest(params, 1000);
}