master
c 382 lines 15.4 KB
Raw
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 }