-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add MCP endpoint for stats
#5341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
643b322
adb059c
6a2f777
7f19b64
ae92ae2
471ebca
2d3d12b
762afaa
33fce1c
1c4bac0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| #ifndef CLASS_STATS_TOOL_HANDLER_H | ||
| #define CLASS_STATS_TOOL_HANDLER_H | ||
|
|
||
| #include <map> | ||
|
|
||
| #include "MCP_Tool_Handler.h" | ||
| #include "MCP_Thread.h" | ||
|
|
||
| /** | ||
| * @brief Stats Tool Handler for /mcp/stats endpoint | ||
| * | ||
| * This handler provides tools for real-time metrics, statistics, and monitoring | ||
| * of ProxySQL internals including connection pools, query digests, errors, | ||
| * cluster status, and more. | ||
| * | ||
| * Tools provided: | ||
| * - get_health: Comprehensive health status summary | ||
| * - show_processlist: Active sessions (like MySQL SHOW PROCESSLIST) | ||
| * - show_metrics: Prometheus-compatible metrics | ||
| * - show_queries: Query digest performance statistics | ||
| * - show_connections: Backend connection pool metrics | ||
| * - show_errors: Error tracking and analysis | ||
| * - show_cluster: Cluster node health and sync status | ||
| * - list_stats: List available statistics tables | ||
| * - get_stats: Ad-hoc query any stats table | ||
| * - show_commands: Command execution statistics with latency distribution | ||
| * - show_users: User connection statistics | ||
| * - show_client_cache: Client host cache for connection throttling | ||
| * - show_gtid: GTID replication information | ||
| * - show_query_rules: Query rule hit statistics | ||
| * - show_history_connections: Historical connection trends | ||
| * - show_history_query_digest: Historical query digest snapshots | ||
| * - aggregate_metrics: Custom metric aggregations | ||
| */ | ||
wazir-ahmed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| class Stats_Tool_Handler : public MCP_Tool_Handler { | ||
| private: | ||
| MCP_Threads_Handler* mcp_handler; ///< Pointer to MCP handler | ||
| pthread_mutex_t handler_lock; ///< Mutex for thread-safe operations | ||
|
|
||
| // Tool handlers | ||
| json handle_get_health(const json& arguments); | ||
| json handle_show_processlist(const json& arguments); | ||
| json handle_show_metrics(const json& arguments); | ||
| json handle_show_queries(const json& arguments); | ||
| json handle_show_connections(const json& arguments); | ||
| json handle_show_errors(const json& arguments); | ||
| json handle_show_cluster(const json& arguments); | ||
| json handle_list_stats(const json& arguments); | ||
| json handle_get_stats(const json& arguments); | ||
| json handle_show_commands(const json& arguments); | ||
| json handle_show_users(const json& arguments); | ||
| json handle_show_client_cache(const json& arguments); | ||
| json handle_show_gtid(const json& arguments); | ||
| json handle_show_query_rules(const json& arguments); | ||
| json handle_show_history_connections(const json& arguments); | ||
| json handle_show_history_query_digest(const json& arguments); | ||
| json handle_aggregate_metrics(const json& arguments); | ||
|
|
||
| // Helper methods | ||
|
|
||
| /** | ||
| * @brief Execute a SQL query against GloAdmin->admindb | ||
| * @param sql The SQL query to execute | ||
| * @param resultset Output pointer for the result set (caller must delete) | ||
| * @param cols Output for number of columns | ||
| * @return Empty string on success, error message on failure | ||
| */ | ||
| std::string execute_admin_query(const char* sql, SQLite3_result** resultset, int* cols); | ||
|
|
||
| /** | ||
| * @brief Execute a SQL query against GloAdmin->statsdb_disk (historical data) | ||
| * @param sql The SQL query to execute | ||
| * @param resultset Output pointer for the result set (caller must delete) | ||
| * @param cols Output for number of columns | ||
| * @return Empty string on success, error message on failure | ||
| */ | ||
| std::string execute_statsdb_disk_query(const char* sql, SQLite3_result** resultset, int* cols); | ||
|
|
||
| /** | ||
| * @brief Parse key-value pairs from stats_*_global tables | ||
| * @param resultset The result set from a global stats query | ||
| * @return Map of variable name to variable value | ||
| */ | ||
| std::map<std::string, std::string> parse_global_stats(SQLite3_result* resultset); | ||
|
|
||
| /** | ||
| * @brief Validate a stats table name against a whitelist | ||
| * @param table The table name to validate | ||
| * @return true if the table name is valid | ||
| */ | ||
| static bool is_valid_stats_table(const std::string& table); | ||
|
|
||
| public: | ||
| /** | ||
| * @brief Constructor | ||
| * @param handler Pointer to MCP_Threads_Handler | ||
| */ | ||
| Stats_Tool_Handler(MCP_Threads_Handler* handler); | ||
|
|
||
| /** | ||
| * @brief Destructor | ||
| */ | ||
| ~Stats_Tool_Handler() override; | ||
|
|
||
| // MCP_Tool_Handler interface implementation | ||
| json get_tool_list() override; | ||
| json get_tool_description(const std::string& tool_name) override; | ||
| json execute_tool(const std::string& tool_name, const json& arguments) override; | ||
| int init() override; | ||
| void close() override; | ||
| std::string get_handler_name() const override { return "stats"; } | ||
| }; | ||
|
|
||
| #endif /* CLASS_STATS_TOOL_HANDLER_H */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -356,6 +356,27 @@ static inline void set_thread_name(const char(&name)[LEN], const bool en = true) | |
| */ | ||
| std::string get_client_addr(struct sockaddr* client_addr); | ||
|
|
||
| /** | ||
| * @brief Escape single quotes in a string for safe SQL insertion. | ||
| * @param input The string to escape. | ||
| * @return A new string with single quotes doubled and backslashes escaped. | ||
| */ | ||
| std::string sql_escape(const std::string& input); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * @brief Calculate an approximate percentile value from histogram bucket counts. | ||
| * @param buckets Vector of counts per histogram bucket. | ||
| * @param thresholds Vector of upper-bound threshold values for each bucket (same length as buckets). | ||
| * @param percentile The percentile to calculate, in the range [0.0, 1.0]. | ||
| * @return The threshold value of the bucket in which the target percentile falls, | ||
| * or 0 if the buckets are empty. | ||
| */ | ||
| int calculate_percentile_from_histogram( | ||
| const std::vector<int>& buckets, | ||
| const std::vector<int>& thresholds, | ||
| double percentile | ||
| ); | ||
|
Comment on lines
+369
to
+381
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Two minor concerns:
💡 Suggested guard and doc note int calculate_percentile_from_histogram(
const std::vector<int>& buckets,
const std::vector<int>& thresholds,
double percentile
) {
+ assert(percentile >= 0.0 && percentile <= 1.0);
long long total = 0;
for (int b : buckets) total += b;
if (total == 0) return 0;
long long target = (long long)(total * percentile);
+ if (target == 0) target = 1; // ensure at least one count is crossed for P0
long long cumulative = 0;🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * @brief Check if a port is available for binding | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #include "sqlite3db.h" | ||
| #include "MCP_Tool_Handler.h" | ||
|
|
||
| #include "../deps/json/json.hpp" | ||
| using json = nlohmann::json; | ||
| #define PROXYJSON | ||
|
|
||
| json MCP_Tool_Handler::resultset_to_json(SQLite3_result* resultset, int cols) { | ||
| json rows = json::array(); | ||
|
|
||
| if (!resultset || resultset->rows_count == 0) { | ||
| return rows; | ||
| } | ||
|
|
||
| for (const auto& row : resultset->rows) { | ||
| json obj = json::object(); | ||
| for (int i = 0; i < cols && i < (int)resultset->column_definition.size(); i++) { | ||
| const char* col_name = resultset->column_definition[i]->name; | ||
| const char* val = row->fields[i]; | ||
|
|
||
| if (!val) { | ||
| obj[col_name] = nullptr; | ||
| continue; | ||
| } | ||
|
|
||
| // Try to parse the value as a number. | ||
| // strtoll / strtod are used directly to avoid the overhead | ||
| // of a separate is_numeric() scan followed by a second parse. | ||
| char* end = nullptr; | ||
| long long ll = strtoll(val, &end, 10); | ||
| if (end != val && *end == '\0') { | ||
| obj[col_name] = ll; | ||
| } else { | ||
| // Not a plain integer; try floating-point | ||
| double d = strtod(val, &end); | ||
| if (end != val && *end == '\0') { | ||
| obj[col_name] = d; | ||
| } else { | ||
| obj[col_name] = std::string(val); | ||
| } | ||
| } | ||
|
Comment on lines
+31
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current numeric parsing logic using A more robust approach is to first attempt parsing as char* end = nullptr;
bool parsed = false;
// Try unsigned long long for non-negative numbers, which is common for stats counters.
if (val[0] != '-') {
errno = 0;
unsigned long long ull = strtoull(val, &end, 10);
if (end != val && *end == '\0' && errno != ERANGE) {
obj[col_name] = ull;
parsed = true;
}
}
// Try long long if not parsed yet (e.g., for negative numbers).
if (!parsed) {
errno = 0;
end = nullptr;
long long ll = strtoll(val, &end, 10);
if (end != val && *end == '\0' && errno != ERANGE) {
obj[col_name] = ll;
parsed = true;
}
}
// Fallback to double if it's not an integer.
if (!parsed) {
end = nullptr;
double d = strtod(val, &end);
if (end != val && *end == '\0') {
obj[col_name] = d;
parsed = true;
}
}
// Finally, treat as a string if no numeric conversion worked.
if (!parsed) {
obj[col_name] = std::string(val);
} |
||
| } | ||
| rows.push_back(obj); | ||
| } | ||
|
|
||
| return rows; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.