| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "mcp-tools-weights.h" |
| 4 | #include "mcp-params.h" |
| 5 | #include "web/api/web_api.h" |
| 6 | #include "web/api/queries/weights.h" |
| 7 | #include "web/api/queries/query.h" |
| 8 | |
| 9 | // Common function to execute the 'weights' request |
| 10 | static MCP_RETURN_CODE execute_weights_request( |
| 11 | MCP_CLIENT *mcpc, |
| 12 | struct json_object *params, |
| 13 | MCP_REQUEST_ID id, |
| 14 | WEIGHTS_METHOD method, |
| 15 | const char *default_time_group |
| 16 | ) { |
| 17 | // Extract time parameters using common parsing functions |
| 18 | time_t after, before; |
| 19 | if (!mcp_params_parse_time_window(params, &after, &before, |
| 20 | MCP_DEFAULT_AFTER_TIME, MCP_DEFAULT_BEFORE_TIME, |
| 21 | false, mcpc->error)) { |
| 22 | return MCP_RC_BAD_REQUEST; |
| 23 | } |
| 24 | time_t baseline_after = 0; |
| 25 | time_t baseline_before = 0; |
| 26 | |
| 27 | // For correlation methods (KS2, VOLUME), parse baseline times |
| 28 | if (method == WEIGHTS_METHOD_MC_KS2 || method == WEIGHTS_METHOD_MC_VOLUME) { |
| 29 | if (!mcp_params_parse_time_window(params, &baseline_after, &baseline_before, |
| 30 | 0, 0, true, mcpc->error)) { |
| 31 | return MCP_RC_BAD_REQUEST; |
| 32 | } |
| 33 | |
| 34 | // If baseline not specified, auto-calculate as 4x the query window before the query window |
| 35 | if (baseline_after == 0 && baseline_before == 0) { |
| 36 | time_t window = before - after; |
| 37 | baseline_before = after; |
| 38 | baseline_after = baseline_before - (window * 4); |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | // Parse filter parameters using common parsing functions |
| 43 | // Use CLEAN_BUFFER for automatic cleanup |
| 44 | CLEAN_BUFFER *metrics_buffer = NULL; |
| 45 | CLEAN_BUFFER *nodes_buffer = NULL; |
| 46 | CLEAN_BUFFER *instances_buffer = NULL; |
| 47 | CLEAN_BUFFER *dimensions_buffer = NULL; |
| 48 | CLEAN_BUFFER *labels_buffer = NULL; |
| 49 | |
| 50 | // Parse metrics as an array |
| 51 | metrics_buffer = mcp_params_parse_array_to_pattern(params, "metrics", false, false, MCP_TOOL_LIST_METRICS, mcpc->error); |
| 52 | if (buffer_strlen(mcpc->error) > 0) { |
| 53 | return MCP_RC_BAD_REQUEST; |
| 54 | } |
| 55 | |
| 56 | // Parse nodes as array |
| 57 | nodes_buffer = mcp_params_parse_array_to_pattern(params, "nodes", false, false, MCP_TOOL_LIST_NODES, mcpc->error); |
| 58 | if (buffer_strlen(mcpc->error) > 0) { |
| 59 | return MCP_RC_BAD_REQUEST; |
| 60 | } |
| 61 | |
| 62 | // Parse instances as an array |
| 63 | instances_buffer = mcp_params_parse_array_to_pattern(params, "instances", false, false, MCP_TOOL_GET_METRICS_DETAILS, mcpc->error); |
| 64 | if (buffer_strlen(mcpc->error) > 0) { |
| 65 | return MCP_RC_BAD_REQUEST; |
| 66 | } |
| 67 | |
| 68 | // Parse dimensions as array |
| 69 | dimensions_buffer = mcp_params_parse_array_to_pattern(params, "dimensions", false, false, MCP_TOOL_GET_METRICS_DETAILS, mcpc->error); |
| 70 | if (buffer_strlen(mcpc->error) > 0) { |
| 71 | return MCP_RC_BAD_REQUEST; |
| 72 | } |
| 73 | |
| 74 | // Parse labels as object |
| 75 | labels_buffer = mcp_params_parse_labels_object(params, MCP_TOOL_GET_METRICS_DETAILS, mcpc->error); |
| 76 | if (buffer_strlen(mcpc->error) > 0) { |
| 77 | return MCP_RC_BAD_REQUEST; |
| 78 | } |
| 79 | |
| 80 | // Get cardinality limit |
| 81 | struct json_object *obj; |
| 82 | size_t cardinality_limit = MCP_WEIGHTS_CARDINALITY_LIMIT; |
| 83 | if (json_object_object_get_ex(params, "cardinality_limit", &obj) && json_object_is_type(obj, json_type_int)) |
| 84 | cardinality_limit = json_object_get_int(obj); |
| 85 | |
| 86 | // Extract timeout parameter |
| 87 | int timeout = mcp_params_extract_timeout(params, "timeout", MCP_DEFAULT_TIMEOUT_WEIGHTS, 1, 3600, mcpc->error); |
| 88 | if (buffer_strlen(mcpc->error) > 0) { |
| 89 | return MCP_RC_BAD_REQUEST; |
| 90 | } |
| 91 | |
| 92 | // Set time_group parameter based on the method |
| 93 | const char *time_group_options = default_time_group; |
| 94 | RRDR_TIME_GROUPING time_group_method = RRDR_GROUPING_AVERAGE; |
| 95 | |
| 96 | // For find_unstable_metrics (WEIGHTS_METHOD_VALUE with cv default), always use CV |
| 97 | if (method == WEIGHTS_METHOD_VALUE && default_time_group && strcmp(default_time_group, "cv") == 0) { |
| 98 | time_group_method = RRDR_GROUPING_CV; |
| 99 | time_group_options = "cv"; |
| 100 | } else if (default_time_group) { |
| 101 | // Use the default time grouping specified by the tool |
| 102 | time_group_method = time_grouping_parse(default_time_group, RRDR_GROUPING_AVERAGE); |
| 103 | } |
| 104 | |
| 105 | // Set options |
| 106 | RRDR_OPTIONS options = RRDR_OPTION_NOT_ALIGNED | RRDR_OPTION_NULL2ZERO | RRDR_OPTION_ABSOLUTE | RRDR_OPTION_NONZERO; |
| 107 | |
| 108 | // Build the 'weights' request structure |
| 109 | QUERY_WEIGHTS_REQUEST qwr = { |
| 110 | .version = 2, |
| 111 | .host = NULL, // Will query all hosts |
| 112 | .scope_nodes = buffer_tostring(nodes_buffer), |
| 113 | .scope_contexts = buffer_tostring(metrics_buffer), |
| 114 | .scope_instances = buffer_tostring(instances_buffer), |
| 115 | .scope_labels = buffer_tostring(labels_buffer), |
| 116 | .scope_dimensions = buffer_tostring(dimensions_buffer), |
| 117 | .nodes = NULL, |
| 118 | // exclude netdata internal metrics |
| 119 | // exclude system interrupts and CPU interrupts, while are fragile |
| 120 | .contexts = "!netdata.*|!system.interrupts|!system.intr|!cpu.interrupts|*", |
| 121 | .instances = NULL, |
| 122 | .dimensions = NULL, |
| 123 | .labels = NULL, |
| 124 | .alerts = NULL, |
| 125 | .group_by = { |
| 126 | .group_by = RRDR_GROUP_BY_NONE, |
| 127 | .group_by_label = NULL, |
| 128 | .aggregation = RRDR_GROUP_BY_FUNCTION_AVERAGE, |
| 129 | }, |
| 130 | .method = method, |
| 131 | .format = WEIGHTS_FORMAT_MCP, |
| 132 | .time_group_method = time_group_method, |
| 133 | .time_group_options = time_group_options, |
| 134 | .baseline_after = baseline_after, |
| 135 | .baseline_before = baseline_before, |
| 136 | .after = after, |
| 137 | .before = before, |
| 138 | .points = 500, // Default points for weights |
| 139 | .options = options, |
| 140 | .tier = 0, |
| 141 | .timeout_ms = (int)(timeout * 1000), // Convert seconds to milliseconds |
| 142 | .cardinality_limit = cardinality_limit, |
| 143 | .interrupt_callback = NULL, |
| 144 | .interrupt_callback_data = NULL, |
| 145 | .transaction = NULL, |
| 146 | }; |
| 147 | |
| 148 | // Create a temporary buffer for the 'weights' API response |
| 149 | CLEAN_BUFFER *tmp_buffer = buffer_create(0, NULL); |
| 150 | |
| 151 | // Call the weights API function with the temporary buffer |
| 152 | int http_code = web_api_v12_weights(tmp_buffer, &qwr); |
| 153 | |
| 154 | // Handle response |
| 155 | if (http_code != HTTP_RESP_OK) { |
| 156 | buffer_flush(mcpc->error); |
| 157 | |
| 158 | switch (http_code) { |
| 159 | case HTTP_RESP_BAD_REQUEST: |
| 160 | buffer_sprintf(mcpc->error, "Invalid request parameters"); |
| 161 | return MCP_RC_BAD_REQUEST; |
| 162 | |
| 163 | case HTTP_RESP_NOT_FOUND: |
| 164 | buffer_sprintf(mcpc->error, "No results found"); |
| 165 | return MCP_RC_NOT_FOUND; |
| 166 | |
| 167 | case HTTP_RESP_GATEWAY_TIMEOUT: |
| 168 | buffer_sprintf(mcpc->error, "Request timed out - repeat the request with a longer timeout"); |
| 169 | return MCP_RC_ERROR; |
| 170 | |
| 171 | default: |
| 172 | buffer_sprintf(mcpc->error, "Internal error (HTTP %d)", http_code); |
| 173 | return MCP_RC_INTERNAL_ERROR; |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | // Initialize response |
| 178 | mcp_init_success_result(mcpc, id); |
| 179 | |
| 180 | // Wrap the response in MCP JSON-RPC format |
| 181 | buffer_json_member_add_array(mcpc->result, "content"); |
| 182 | { |
| 183 | buffer_json_add_array_item_object(mcpc->result); |
| 184 | { |
| 185 | buffer_json_member_add_string(mcpc->result, "type", "text"); |
| 186 | buffer_json_member_add_string(mcpc->result, "text", buffer_tostring(tmp_buffer)); |
| 187 | } |
| 188 | buffer_json_object_close(mcpc->result); |
| 189 | } |
| 190 | buffer_json_array_close(mcpc->result); |
| 191 | |
| 192 | // Close the result object and finalize JSON |
| 193 | buffer_json_object_close(mcpc->result); // Close the "result" object |
| 194 | buffer_json_finalize(mcpc->result); |
| 195 | |
| 196 | return MCP_RC_OK; |
| 197 | } |
| 198 | |
| 199 | // Schema helper for common time window parameters |
| 200 | static void add_weights_time_parameters(BUFFER *buffer, bool include_baseline, bool required) { |
| 201 | |
| 202 | // add 'after' and 'before' parameters |
| 203 | mcp_schema_add_time_params(buffer, "metrics", required); |
| 204 | |
| 205 | if (include_baseline) { |
| 206 | mcp_schema_add_time_param( |
| 207 | buffer, "baseline_after", |
| 208 | "Baseline start time", |
| 209 | "Start time for the baseline period to compare against. If not specified, " |
| 210 | "automatically set to 4x the query window before the query period.", |
| 211 | "'baseline_before'", |
| 212 | 0, |
| 213 | required); |
| 214 | |
| 215 | mcp_schema_add_time_param( |
| 216 | buffer, "baseline_before", |
| 217 | "Baseline end time", |
| 218 | "End time for the baseline period. If not specified, automatically set to " |
| 219 | "the start of the query period (adjacent to 'after').", |
| 220 | "'after'", |
| 221 | 0, |
| 222 | required); |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | // Schema helper for common filter parameters |
| 227 | static void add_weights_filter_parameters(BUFFER *buffer) { |
| 228 | mcp_schema_add_array_param( |
| 229 | buffer, "metrics", |
| 230 | "Filter by metrics", |
| 231 | "Array of metrics (contexts) to filter (e.g., ['system.cpu', 'disk.io', 'mysql.queries']). Use '" MCP_TOOL_LIST_METRICS "' to discover available metrics."); |
| 232 | |
| 233 | mcp_schema_add_array_param( |
| 234 | buffer, "nodes", |
| 235 | "Filter by nodes", |
| 236 | "Array of nodes to filter (e.g., ['web-server-1', 'database-primary']). Use '" MCP_TOOL_LIST_NODES "' to discover available nodes."); |
| 237 | |
| 238 | mcp_schema_add_array_param( |
| 239 | buffer, "instances", |
| 240 | "Filter by instances", |
| 241 | "Array of metric instances to filter (e.g., ['eth0', 'sda', 'production_db']). Use '" MCP_TOOL_GET_METRICS_DETAILS "' to discover instances for a metric."); |
| 242 | |
| 243 | mcp_schema_add_array_param(buffer, "dimensions", |
| 244 | "Filter by dimensions", |
| 245 | "Array of dimension names to filter (e.g., ['user', 'writes', 'slow_queries']). Use '" MCP_TOOL_GET_METRICS_DETAILS "' to discover dimensions for a metric."); |
| 246 | |
| 247 | mcp_schema_add_labels_object(buffer, |
| 248 | "Filter by labels", |
| 249 | "Filter using labels where each key maps to an array of exact values. " |
| 250 | "Values in the same array are ORed, different keys are ANDed. " |
| 251 | "Example: {\"disk_type\": [\"ssd\", \"nvme\"], \"mount_point\": [\"/\"]}\n" |
| 252 | "Note: Wildcards are not supported. Use exact label keys and values only. " |
| 253 | "Use '" MCP_TOOL_GET_METRICS_DETAILS "' to discover available labels."); |
| 254 | } |
| 255 | |
| 256 | static void add_weights_common_parameters(BUFFER *buffer) { |
| 257 | mcp_schema_add_cardinality_limit( |
| 258 | buffer, "Maximum number of results to return", |
| 259 | MCP_WEIGHTS_CARDINALITY_LIMIT, |
| 260 | 30, // minimum for weights |
| 261 | MAX(MCP_WEIGHTS_CARDINALITY_LIMIT, MCP_WEIGHTS_CARDINALITY_LIMIT_MAX)); |
| 262 | |
| 263 | // Timeout parameter |
| 264 | mcp_schema_add_timeout( |
| 265 | buffer, "timeout", |
| 266 | "Query timeout", |
| 267 | "Maximum time to wait for the query to complete (in seconds)", |
| 268 | MCP_DEFAULT_TIMEOUT_WEIGHTS, 1, 3600, false); |
| 269 | } |
| 270 | |
| 271 | // find_correlated_metrics implementation |
| 272 | MCP_RETURN_CODE mcp_tool_find_correlated_metrics_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 273 | // Parse method parameter |
| 274 | WEIGHTS_METHOD method = WEIGHTS_METHOD_MC_VOLUME; // Default to volume as per schema |
| 275 | |
| 276 | struct json_object *obj; |
| 277 | if (json_object_object_get_ex(params, "method", &obj) && json_object_is_type(obj, json_type_string)) { |
| 278 | const char *method_str = json_object_get_string(obj); |
| 279 | if (strcmp(method_str, "ks2") == 0) |
| 280 | method = WEIGHTS_METHOD_MC_KS2; |
| 281 | else if (strcmp(method_str, "volume") == 0) |
| 282 | method = WEIGHTS_METHOD_MC_VOLUME; |
| 283 | } |
| 284 | |
| 285 | return execute_weights_request(mcpc, params, id, method, NULL); |
| 286 | } |
| 287 | |
| 288 | void mcp_tool_find_correlated_metrics_schema(BUFFER *buffer) { |
| 289 | buffer_json_member_add_object(buffer, "inputSchema"); |
| 290 | buffer_json_member_add_string(buffer, "type", "object"); |
| 291 | buffer_json_member_add_string(buffer, "title", "Find metrics that changed during an incident"); |
| 292 | |
| 293 | buffer_json_member_add_object(buffer, "properties"); |
| 294 | { |
| 295 | add_weights_time_parameters(buffer, true, true); // include_baseline=true, required=true |
| 296 | add_weights_filter_parameters(buffer); |
| 297 | |
| 298 | buffer_json_member_add_object(buffer, "method"); |
| 299 | buffer_json_member_add_string(buffer, "type", "string"); |
| 300 | buffer_json_member_add_string(buffer, "title", "Correlation method"); |
| 301 | buffer_json_member_add_string( |
| 302 | buffer, "description", |
| 303 | "Algorithm to use:\n" |
| 304 | "- 'ks2': Statistical distribution comparison (slow, but intelligent)\n" |
| 305 | "- 'volume': Percentage change in averages (fast, works well for most cases)"); |
| 306 | buffer_json_member_add_array(buffer, "enum"); |
| 307 | buffer_json_add_array_item_string(buffer, "ks2"); |
| 308 | buffer_json_add_array_item_string(buffer, "volume"); |
| 309 | buffer_json_array_close(buffer); |
| 310 | buffer_json_member_add_string(buffer, "default", "volume"); |
| 311 | buffer_json_object_close(buffer); // method |
| 312 | |
| 313 | add_weights_common_parameters(buffer); |
| 314 | } |
| 315 | buffer_json_object_close(buffer); // properties |
| 316 | |
| 317 | buffer_json_member_add_array(buffer, "required"); |
| 318 | { |
| 319 | buffer_json_add_array_item_string(buffer, "after"); |
| 320 | buffer_json_add_array_item_string(buffer, "before"); |
| 321 | } |
| 322 | buffer_json_array_close(buffer); |
| 323 | |
| 324 | buffer_json_object_close(buffer); // inputSchema |
| 325 | } |
| 326 | |
| 327 | // find_anomalous_metrics implementation |
| 328 | MCP_RETURN_CODE mcp_tool_find_anomalous_metrics_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 329 | return execute_weights_request(mcpc, params, id, WEIGHTS_METHOD_ANOMALY_RATE, NULL); |
| 330 | } |
| 331 | |
| 332 | void mcp_tool_find_anomalous_metrics_schema(BUFFER *buffer) { |
| 333 | buffer_json_member_add_object(buffer, "inputSchema"); |
| 334 | buffer_json_member_add_string(buffer, "type", "object"); |
| 335 | buffer_json_member_add_string(buffer, "title", "Find metrics with highest anomaly rates"); |
| 336 | |
| 337 | buffer_json_member_add_object(buffer, "properties"); |
| 338 | { |
| 339 | add_weights_time_parameters(buffer, false, true); // include_baseline=false, required=true |
| 340 | add_weights_filter_parameters(buffer); |
| 341 | add_weights_common_parameters(buffer); |
| 342 | } |
| 343 | buffer_json_object_close(buffer); // properties |
| 344 | |
| 345 | buffer_json_member_add_array(buffer, "required"); |
| 346 | { |
| 347 | buffer_json_add_array_item_string(buffer, "after"); |
| 348 | buffer_json_add_array_item_string(buffer, "before"); |
| 349 | } |
| 350 | buffer_json_array_close(buffer); |
| 351 | |
| 352 | buffer_json_object_close(buffer); // inputSchema |
| 353 | } |
| 354 | |
| 355 | // find_unstable_metrics implementation |
| 356 | MCP_RETURN_CODE mcp_tool_find_unstable_metrics_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) { |
| 357 | // Use coefficient of variation for finding unstable metrics |
| 358 | return execute_weights_request(mcpc, params, id, WEIGHTS_METHOD_VALUE, "cv"); |
| 359 | } |
| 360 | |
| 361 | void mcp_tool_find_unstable_metrics_schema(BUFFER *buffer) { |
| 362 | buffer_json_member_add_object(buffer, "inputSchema"); |
| 363 | buffer_json_member_add_string(buffer, "type", "object"); |
| 364 | buffer_json_member_add_string(buffer, "title", "Find metrics with high variability"); |
| 365 | |
| 366 | buffer_json_member_add_object(buffer, "properties"); |
| 367 | { |
| 368 | add_weights_time_parameters(buffer, false, true); // include_baseline=false, required=true |
| 369 | add_weights_filter_parameters(buffer); |
| 370 | add_weights_common_parameters(buffer); |
| 371 | } |
| 372 | buffer_json_object_close(buffer); // properties |
| 373 | |
| 374 | buffer_json_member_add_array(buffer, "required"); |
| 375 | { |
| 376 | buffer_json_add_array_item_string(buffer, "after"); |
| 377 | buffer_json_add_array_item_string(buffer, "before"); |
| 378 | } |
| 379 | buffer_json_array_close(buffer); |
| 380 | |
| 381 | buffer_json_object_close(buffer); // inputSchema |
| 382 | } |