mirror of https://github.com/sysown/proxysql
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
374 lines
12 KiB
374 lines
12 KiB
/**
|
|
* @file mcp_stats_refresh-t.cpp
|
|
* @brief TAP integration test for MCP stats endpoint functionality.
|
|
*
|
|
* This test validates that the MCP stats endpoint can query runtime metrics
|
|
* and that the metrics reflect actual ProxySQL state.
|
|
*
|
|
* Test strategy:
|
|
* 1. Query Client_Connections_connected via MCP stats endpoint
|
|
* 2. Create several new MySQL connections to generate traffic
|
|
* 3. Query the same metric again
|
|
* 4. Verify the connection count has increased
|
|
*/
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "mysql.h"
|
|
#include "tap.h"
|
|
#include "command_line.h"
|
|
#include "utils.h"
|
|
#include "mcp_client.h"
|
|
|
|
using json = nlohmann::json;
|
|
|
|
namespace {
|
|
|
|
/**
|
|
* @brief Execute an admin SQL statement and report success/failure.
|
|
*
|
|
* @param admin Open admin connection.
|
|
* @param query SQL statement to execute.
|
|
* @param context Human-readable label used in diagnostics.
|
|
* @return true on success, false on failure.
|
|
*/
|
|
bool run_admin_stmt(MYSQL* admin, const std::string& query, const char* context) {
|
|
if (!admin) {
|
|
diag("%s: admin connection is null", context);
|
|
return false;
|
|
}
|
|
if (run_q(admin, query.c_str()) != 0) {
|
|
diag("%s failed: %s", context, mysql_error(admin));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Configure MCP runtime variables required by this test.
|
|
*
|
|
* @param admin Open admin connection.
|
|
* @param cl TAP command-line environment with target MCP port.
|
|
* @return true if all configuration statements succeeded.
|
|
*/
|
|
bool configure_mcp_stats_endpoint(MYSQL* admin, const CommandLine& cl) {
|
|
const std::vector<std::string> statements = {
|
|
"SET mcp-port=" + std::to_string(cl.mcp_port),
|
|
"SET mcp-use_ssl=false",
|
|
"SET mcp-enabled=true",
|
|
"SET mcp-stats_endpoint_auth=''",
|
|
"LOAD MCP VARIABLES TO RUNTIME"
|
|
};
|
|
|
|
for (const auto& stmt : statements) {
|
|
if (!run_admin_stmt(admin, stmt, "MCP stats config")) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Parse and validate the payload returned by MCP `show_status`.
|
|
*
|
|
* Expected payload shape (after MCPClient extracts from content[0].text):
|
|
* `{ "db_type": "mysql", "variables": [...] }`
|
|
*
|
|
* @param response MCP response object.
|
|
* @param variables Output JSON array of variables.
|
|
* @param error Output error text on failure.
|
|
* @return true when payload structure is valid.
|
|
*/
|
|
bool extract_show_status_variables(const MCPResponse& response, json& variables, std::string& error) {
|
|
if (!response.is_success()) {
|
|
error = response.get_error_message();
|
|
return false;
|
|
}
|
|
|
|
const json& payload = response.get_result();
|
|
if (!payload.is_object()) {
|
|
error = "show_status payload is not a JSON object";
|
|
return false;
|
|
}
|
|
|
|
// Check for variables array directly (new format)
|
|
if (payload.contains("variables") && payload["variables"].is_array()) {
|
|
variables = payload["variables"];
|
|
return true;
|
|
}
|
|
|
|
// Check for legacy format with success/result wrapper
|
|
if (!payload.value("success", false)) {
|
|
error = payload.value("error", std::string("show_status returned tool error"));
|
|
return false;
|
|
}
|
|
|
|
if (!payload.contains("result") || !payload["result"].is_object()) {
|
|
error = "show_status payload missing object field 'result'";
|
|
return false;
|
|
}
|
|
|
|
const json& result_obj = payload["result"];
|
|
if (!result_obj.contains("variables") || !result_obj["variables"].is_array()) {
|
|
error = "show_status payload missing array field 'result.variables'";
|
|
return false;
|
|
}
|
|
|
|
variables = result_obj["variables"];
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Extract a numeric metric value from show_status variables array.
|
|
*
|
|
* @param variables JSON array of variable objects (each with variable_name/value or Variable_Name/Variable_Value).
|
|
* @param var_name Name of the variable to find.
|
|
* @param value Output value if found.
|
|
* @return true if variable was found and parsed as integer.
|
|
*/
|
|
bool get_metric_value(const json& variables, const std::string& var_name, long& value) {
|
|
for (const auto& var : variables) {
|
|
// Try lowercase field names (new format)
|
|
if (var.contains("variable_name") && var["variable_name"] == var_name) {
|
|
if (var.contains("value")) {
|
|
try {
|
|
value = std::stol(var["value"].get<std::string>());
|
|
return true;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// Try uppercase field names (legacy format)
|
|
if (var.contains("Variable_Name") && var["Variable_Name"] == var_name) {
|
|
if (var.contains("Variable_Value")) {
|
|
try {
|
|
value = std::stol(var["Variable_Value"].get<std::string>());
|
|
return true;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
plan(10);
|
|
|
|
CommandLine cl;
|
|
if (cl.getEnv()) {
|
|
diag("Failed to read TAP environment");
|
|
return exit_status();
|
|
}
|
|
|
|
diag("=== MCP Stats Refresh Test ===");
|
|
diag("This test validates that the MCP stats endpoint can query runtime metrics");
|
|
diag("and that the metrics reflect actual ProxySQL state.");
|
|
diag("Test strategy:");
|
|
diag(" 1. Query Client_Connections_connected via MCP stats endpoint");
|
|
diag(" 2. Create several new MySQL connections to generate traffic");
|
|
diag(" 3. Query the same metric again");
|
|
diag(" 4. Verify the connection count has increased");
|
|
diag("===============================");
|
|
|
|
MYSQL* admin = nullptr;
|
|
MCPClient* mcp = nullptr;
|
|
bool can_continue = true;
|
|
|
|
admin = init_mysql_conn(cl.admin_host, cl.admin_port, cl.admin_username, cl.admin_password);
|
|
ok(admin != nullptr, "Admin connection established");
|
|
if (!admin) {
|
|
skip(9, "Cannot continue without admin connection");
|
|
can_continue = false;
|
|
}
|
|
|
|
bool configured = false;
|
|
if (can_continue) {
|
|
configured = configure_mcp_stats_endpoint(admin, cl);
|
|
ok(configured, "Configured and loaded MCP runtime variables for /mcp/stats");
|
|
if (!configured) {
|
|
skip(8, "Cannot continue without MCP runtime configuration");
|
|
can_continue = false;
|
|
}
|
|
}
|
|
|
|
if (can_continue) {
|
|
mcp = new MCPClient(cl.admin_host, cl.mcp_port);
|
|
if (strlen(cl.mcp_auth_token) > 0) {
|
|
mcp->set_auth_token(cl.mcp_auth_token);
|
|
}
|
|
}
|
|
|
|
bool mcp_reachable = false;
|
|
bool using_ssl = false;
|
|
if (can_continue) {
|
|
// Retry loop: MCP server may need a moment to start after LOAD MCP VARIABLES TO RUNTIME
|
|
const int k_max_retries = 30; // 30 retries * 100ms = 3 seconds max wait
|
|
const int k_retry_delay_ms = 100;
|
|
int retry_count = 0;
|
|
|
|
// First try HTTP, then HTTPS if HTTP fails
|
|
for (int ssl_attempt = 0; ssl_attempt <= 1 && !mcp_reachable; ssl_attempt++) {
|
|
bool try_ssl = (ssl_attempt == 1);
|
|
mcp->set_use_ssl(try_ssl);
|
|
|
|
if (try_ssl) {
|
|
diag("HTTP failed, trying HTTPS...");
|
|
}
|
|
|
|
retry_count = 0;
|
|
while (!mcp_reachable && retry_count < k_max_retries) {
|
|
usleep(k_retry_delay_ms * 1000);
|
|
mcp_reachable = mcp->check_server();
|
|
retry_count++;
|
|
}
|
|
|
|
if (mcp_reachable) {
|
|
using_ssl = try_ssl;
|
|
diag("MCP server reachable via %s after %d retries (%dms)",
|
|
try_ssl ? "HTTPS" : "HTTP", retry_count, retry_count * k_retry_delay_ms);
|
|
}
|
|
}
|
|
|
|
ok(mcp_reachable, "MCP server reachable at %s (%s)", mcp->get_connection_info().c_str(),
|
|
using_ssl ? "HTTPS" : "HTTP");
|
|
if (!mcp_reachable) {
|
|
skip(7, "Cannot continue without MCP connectivity");
|
|
can_continue = false;
|
|
}
|
|
}
|
|
|
|
// Variables needed across multiple blocks
|
|
json initial_vars = json::array();
|
|
long initial_count = 0;
|
|
|
|
// Test: Query Client_Connections_connected, create connections, verify count increases
|
|
if (can_continue) {
|
|
diag("Step 1: Querying initial Client_Connections_connected via MCP stats endpoint");
|
|
|
|
// Step 1: Get initial connection count via MCP stats
|
|
const MCPResponse initial_resp = mcp->call_tool(
|
|
"stats",
|
|
"show_status",
|
|
json{{"db_type", "mysql"}, {"variable_name", "Client_Connections_connected"}}
|
|
);
|
|
ok(initial_resp.is_success(), "MCP call stats.show_status(Client_Connections_connected) transport success");
|
|
|
|
// Debug: print raw response
|
|
diag("Raw HTTP response code: %ld", initial_resp.get_http_code());
|
|
if (!initial_resp.is_success()) {
|
|
diag("Transport/protocol error: %s", initial_resp.get_error_message().c_str());
|
|
}
|
|
|
|
std::string initial_err;
|
|
bool initial_payload_ok = extract_show_status_variables(initial_resp, initial_vars, initial_err);
|
|
if (!initial_payload_ok) {
|
|
diag("Payload extraction failed: %s", initial_err.c_str());
|
|
diag("Raw response body: %s", initial_resp.get_http_response().substr(0, 500).c_str());
|
|
}
|
|
ok(initial_payload_ok, "Initial show_status payload valid%s%s",
|
|
initial_payload_ok ? "" : ": ", initial_payload_ok ? "" : initial_err.c_str());
|
|
|
|
if (initial_payload_ok) {
|
|
diag("Received %zu variables in response", initial_vars.size());
|
|
bool found = get_metric_value(initial_vars, "Client_Connections_connected", initial_count);
|
|
if (found) {
|
|
diag("Client_Connections_connected initial value: %ld", initial_count);
|
|
}
|
|
ok(found, "Found Client_Connections_connected in initial response (value=%ld)", initial_count);
|
|
if (!found) {
|
|
skip(4, "Cannot continue without initial connection count");
|
|
can_continue = false;
|
|
}
|
|
} else {
|
|
skip(5, "Cannot continue without valid initial payload");
|
|
can_continue = false;
|
|
}
|
|
}
|
|
|
|
// Create additional connections and verify count increases
|
|
if (can_continue) {
|
|
// Step 2: Create several new MySQL connections to the frontend port
|
|
const int k_new_connections = 5;
|
|
std::vector<MYSQL*> new_conns;
|
|
|
|
diag("Step 2: Creating %d new MySQL connections to %s:%d to generate traffic",
|
|
k_new_connections, cl.host, cl.port);
|
|
for (int i = 0; i < k_new_connections; i++) {
|
|
MYSQL* conn = init_mysql_conn(cl.host, cl.port, cl.username, cl.password);
|
|
if (conn) {
|
|
new_conns.push_back(conn);
|
|
diag(" Created connection %d", i + 1);
|
|
} else {
|
|
diag(" Failed to create connection %d", i + 1);
|
|
}
|
|
}
|
|
diag("Successfully created %zu new connections", new_conns.size());
|
|
ok(new_conns.size() >= 1, "Created at least 1 new connection (created %zu)", new_conns.size());
|
|
|
|
// Step 3: Query connection count again
|
|
diag("Step 3: Querying Client_Connections_connected again via MCP stats endpoint");
|
|
const MCPResponse updated_resp = mcp->call_tool(
|
|
"stats",
|
|
"show_status",
|
|
json{{"db_type", "mysql"}, {"variable_name", "Client_Connections_connected"}}
|
|
);
|
|
ok(updated_resp.is_success(), "MCP call stats.show_status after connections transport success");
|
|
|
|
json updated_vars = json::array();
|
|
std::string updated_err;
|
|
bool updated_payload_ok = extract_show_status_variables(updated_resp, updated_vars, updated_err);
|
|
if (!updated_payload_ok) {
|
|
diag("Payload extraction failed: %s", updated_err.c_str());
|
|
diag("Raw response body: %s", updated_resp.get_http_response().substr(0, 500).c_str());
|
|
}
|
|
ok(updated_payload_ok, "Updated show_status payload valid%s%s",
|
|
updated_payload_ok ? "" : ": ", updated_payload_ok ? "" : updated_err.c_str());
|
|
|
|
// Step 4: Verify count increased
|
|
if (updated_payload_ok) {
|
|
long updated_count = 0;
|
|
bool found = get_metric_value(updated_vars, "Client_Connections_connected", updated_count);
|
|
if (found) {
|
|
diag("Client_Connections_connected updated value: %ld (initial was: %ld)", updated_count, initial_count);
|
|
ok(updated_count > initial_count,
|
|
"Connection count increased after creating connections (before=%ld, after=%ld, diff=%ld)",
|
|
initial_count, updated_count, updated_count - initial_count);
|
|
} else {
|
|
diag("Client_Connections_connected not found in response");
|
|
diag("Available variables: %s", updated_vars.dump().substr(0, 500).c_str());
|
|
ok(false, "Client_Connections_connected not found in updated response");
|
|
}
|
|
} else {
|
|
skip(1, "Cannot verify count without valid updated payload");
|
|
}
|
|
|
|
// Cleanup: close the connections we created
|
|
diag("Cleanup: Closing %zu test connections", new_conns.size());
|
|
for (MYSQL* conn : new_conns) {
|
|
mysql_close(conn);
|
|
}
|
|
}
|
|
|
|
if (admin) {
|
|
run_q(admin, "SET mcp-stats_endpoint_auth=''");
|
|
run_q(admin, "SET mcp-enabled=false");
|
|
run_q(admin, "LOAD MCP VARIABLES TO RUNTIME");
|
|
mysql_close(admin);
|
|
}
|
|
|
|
if (mcp) {
|
|
delete mcp;
|
|
}
|
|
|
|
return exit_status();
|
|
}
|