#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 #include #include #include #include #include #include #include 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; }; 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(rpc.at("error").as_object().at(JS(started))), "512" ); EXPECT_EQ( boost::json::value_to(rpc.at("error").as_object().at(JS(finished))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("error").as_object().at(JS(errored))), "512" ); EXPECT_EQ(boost::json::value_to(rpc.at("error").as_object().at("forwarded")), "0"); EXPECT_EQ( boost::json::value_to(rpc.at("error").as_object().at("failed_forward")), "0" ); EXPECT_EQ(boost::json::value_to(rpc.at("error").as_object().at(JS(failed))), "0"); EXPECT_EQ( boost::json::value_to(rpc.at("complete").as_object().at(JS(started))), "512" ); EXPECT_EQ( boost::json::value_to(rpc.at("complete").as_object().at(JS(finished))), "512" ); EXPECT_EQ( boost::json::value_to(rpc.at("complete").as_object().at(JS(errored))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("complete").as_object().at("forwarded")), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("complete").as_object().at("failed_forward")), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("complete").as_object().at(JS(failed))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("complete").as_object().at(JS(duration_us))), "512000" ); // 1000 per call EXPECT_EQ( boost::json::value_to(rpc.at("forward").as_object().at(JS(started))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("forward").as_object().at(JS(finished))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("forward").as_object().at(JS(errored))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("forward").as_object().at("forwarded")), "512" ); EXPECT_EQ( boost::json::value_to(rpc.at("forward").as_object().at("failed_forward")), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("forward").as_object().at(JS(failed))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("failed").as_object().at(JS(started))), "512" ); EXPECT_EQ( boost::json::value_to(rpc.at("failed").as_object().at(JS(finished))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("failed").as_object().at(JS(errored))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("failed").as_object().at("forwarded")), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("failed").as_object().at("failed_forward")), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("failed").as_object().at(JS(failed))), "512" ); EXPECT_EQ( boost::json::value_to(rpc.at("failedToForward").as_object().at(JS(started))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("failedToForward").as_object().at(JS(finished))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("failedToForward").as_object().at(JS(errored))), "0" ); EXPECT_EQ( boost::json::value_to(rpc.at("failedToForward").as_object().at("forwarded")), "0" ); EXPECT_EQ( boost::json::value_to( rpc.at("failedToForward").as_object().at("failed_forward") ), "512" ); EXPECT_EQ( boost::json::value_to(rpc.at("failedToForward").as_object().at(JS(failed))), "0" ); EXPECT_EQ(boost::json::value_to(report.at("too_busy_errors")), "512"); EXPECT_EQ(boost::json::value_to(report.at("not_ready_errors")), "512"); EXPECT_EQ(boost::json::value_to(report.at("bad_syntax_errors")), "512"); EXPECT_EQ(boost::json::value_to(report.at("unknown_command_errors")), "512"); EXPECT_EQ(boost::json::value_to(report.at("internal_errors")), "512"); } struct RPCCountersMockPrometheusTests : WithMockPrometheus, RPCCountersBaseTest { Counters counters{workQueueMock}; }; TEST_F(RPCCountersMockPrometheusTests, rpcFailed) { auto& startedMock = makeMock("rpc_method_total_number", "{method=\"test\",status=\"started\"}"); auto& failedMock = makeMock("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("rpc_method_total_number", "{method=\"test\",status=\"started\"}"); auto& erroredMock = makeMock("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("rpc_method_total_number", "{method=\"test\",status=\"started\"}"); auto& finishedMock = makeMock("rpc_method_total_number", "{method=\"test\",status=\"finished\"}"); auto& durationMock = makeMock("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("rpc_method_total_number", "{method=\"test\",status=\"forwarded\"}"); EXPECT_CALL(forwardedMock, add(1)); counters.rpcForwarded("test"); } TEST_F(RPCCountersMockPrometheusTests, rpcFailedToForwarded) { auto& failedForwardMock = makeMock( "rpc_method_total_number", "{method=\"test\",status=\"failed_forward\"}" ); EXPECT_CALL(failedForwardMock, add(1)); counters.rpcFailedToForward("test"); } TEST_F(RPCCountersMockPrometheusTests, onTooBusy) { auto& tooBusyMock = makeMock("rpc_error_total_number", "{error_type=\"too_busy\"}"); EXPECT_CALL(tooBusyMock, add(1)); counters.onTooBusy(); } TEST_F(RPCCountersMockPrometheusTests, onNotReady) { auto& notReadyMock = makeMock("rpc_error_total_number", "{error_type=\"not_ready\"}"); EXPECT_CALL(notReadyMock, add(1)); counters.onNotReady(); } TEST_F(RPCCountersMockPrometheusTests, onBadSyntax) { auto& badSyntaxMock = makeMock("rpc_error_total_number", "{error_type=\"bad_syntax\"}"); EXPECT_CALL(badSyntaxMock, add(1)); counters.onBadSyntax(); } TEST_F(RPCCountersMockPrometheusTests, onUnknownCommand) { auto& unknownCommandMock = makeMock("rpc_error_total_number", "{error_type=\"unknown_command\"}"); EXPECT_CALL(unknownCommandMock, add(1)); counters.onUnknownCommand(); } TEST_F(RPCCountersMockPrometheusTests, onInternalError) { auto& internalErrorMock = makeMock("rpc_error_total_number", "{error_type=\"internal_error\"}"); EXPECT_CALL(internalErrorMock, add(1)); counters.onInternalError(); } struct RPCCountersMockPrometheusRecotdLedgerRequestTest : RPCCountersMockPrometheusTests { testing::StrictMock>& ageLedgersHistogramMock = makeMock("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("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("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("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("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); }