master
c 689 lines 34.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 /**
4 * MCP Metrics Query Tool
5 *
6 * This tool allows querying metrics data via the Model Context Protocol.
7 * It provides an interface to the data query engine similar to the API v2 data endpoint.
8 *
9 * Query Process:
10 * 1. The query engine first determines all unique time-series to query by filtering based on context, nodes,
11 * time-frame, and other supplied filters.
12 *
13 * 2. It then queries each time-series, automatically applying over-time-aggregation. For example, if the
14 * database has 1000 points for a time series and you request 10 points, the query engine reduces the
15 * 1000 points to 10 using the time_group aggregation function (average, max, min, etc.).
16 *
17 * 3. After time aggregation, the query engine applies the group_by aggregation across metrics.
18 * For example, if querying disk I/O for 10 disks from 2 nodes with 2 dimensions each (read/write),
19 * you have 40 unique time-series. With group_by=dimension, the engine would:
20 * - Aggregate all 20 'read' dimensions (from all disks across all nodes) into a single 'read' dimension
21 * - Aggregate all 20 'write' dimensions (from all disks across all nodes) into a single 'write' dimension
22 * - Use the specified aggregation function (sum, min, max, average) for this cross-metric aggregation
23 *
24 * 4. The result will contain only the grouped dimensions, but with rich metadata:
25 * - Each data point contains: timestamp, aggregated value, anomaly rate, and quality flags
26 * - Quality flags indicate whether original data had gaps or counter overflows
27 *
28 * 5. When 'jsonwrap' is included in options, the response includes comprehensive statistics about all
29 * facets of the query, providing aggregated min, max, average, anomaly rate, and volume contribution
30 * percentages per node, instance, dimension, and label.
31 */
32
33 #include "mcp-tools-query-metrics.h"
34 #include "mcp-tools.h"
35 #include "mcp-params.h"
36 #include "web/api/formatters/rrd2json.h"
37
38
39 // JSON schema for the metrics query tool
40 void mcp_tool_query_metrics_schema(BUFFER *buffer) {
41 // Tool input schema
42 buffer_json_member_add_object(buffer, "inputSchema");
43 buffer_json_member_add_string(buffer, "type", "object");
44 buffer_json_member_add_string(buffer, "title", "Query Metrics Data");
45
46 // Properties
47 buffer_json_member_add_object(buffer, "properties");
48
49 // Selection parameters
50 mcp_schema_add_string_param(
51 buffer, "metric", "Metric Name",
52 "The exact metric (context) to query.\n"
53 "Use the '" MCP_TOOL_LIST_METRICS "' tool to discover available metrics.",
54 NULL, true);
55
56 mcp_schema_add_array_param(
57 buffer, "dimensions",
58 "Dimensions Filter",
59 "Array of dimensions to include in the query.\n"
60 "Examples: [\"read\", \"write\"] or [\"in\", \"out\"] or [\"used\", \"free\", \"cached\"]\n"
61 "Use the '" MCP_TOOL_GET_METRICS_DETAILS "' tool to discover the available dimensions for a metric.");
62
63 mcp_schema_add_labels_object(
64 buffer, "Labels Filter",
65 "Query only the instances with the given labels. "
66 "Example: {\"disk_type\": [\"ssd\", \"nvme\"], \"mount_point\": [\"/\"]}\n"
67 "Values in the same array are ORed, different keys are ANDed. "
68 "Use the '" MCP_TOOL_GET_METRICS_DETAILS "' tool to discover available labels and values for a metric.");
69
70 mcp_schema_add_array_param(
71 buffer, "instances",
72 "Instances Filter",
73 "Query only the given instances.\n"
74 "Use the '" MCP_TOOL_GET_METRICS_DETAILS "' tool to discover available instances for a metric.\n"
75 "If no instances are specified, all instances of the metric are queried.\n"
76 "Example: [\"instance1\", \"instance2\", \"instance3\"]\n."
77 "IMPORTANT: when you have a choice, prefer to filter by labels instead of instances, because many monitored "
78 "components may change instance names over time.");
79
80 mcp_schema_add_array_param(
81 buffer, "nodes",
82 "Nodes Filter",
83 "Array of nodes to include in the query.\n"
84 "If no nodes are specified, all nodes having data for the given metrics in the specified time-frame will be queried.\n"
85 "Examples: [\"node1\", \"node2\", \"node3\"]\n"
86 "Use the '" MCP_TOOL_LIST_NODES "' tool to discover the available nodes.");
87
88 // Add cardinality limit
89 mcp_schema_add_cardinality_limit(buffer,
90 "Limit the response cardinality (number of dimensions, instances, labels, etc.). "
91 "When the limit is exceeded, the response will indicate how many items were omitted.",
92 MCP_DATA_CARDINALITY_LIMIT,
93 1, // minimum
94 MAX(MCP_DATA_CARDINALITY_LIMIT, MCP_DATA_CARDINALITY_LIMIT_MAX));
95
96 // Time parameters
97 mcp_schema_add_time_params(buffer, "query window", true);
98
99 buffer_json_member_add_object(buffer, "points");
100 {
101 buffer_json_member_add_string(buffer, "type", "number");
102 buffer_json_member_add_string(buffer, "title", "Data Points");
103 buffer_json_member_add_string(buffer, "description", "Number of data points to return.");
104 buffer_json_member_add_uint64(buffer, "default", 60);
105 }
106 buffer_json_object_close(buffer); // points
107
108 buffer_json_member_add_object(buffer, "timeout");
109 {
110 buffer_json_member_add_string(buffer, "type", "number");
111 buffer_json_member_add_string(buffer, "title", "Timeout");
112 buffer_json_member_add_string(buffer, "description", "Query timeout in seconds.");
113 buffer_json_member_add_uint64(buffer, "default", 60);
114 }
115 buffer_json_object_close(buffer); // timeout
116
117 buffer_json_member_add_object(buffer, "options");
118 {
119 buffer_json_member_add_string(buffer, "type", "string");
120 buffer_json_member_add_string(buffer, "title", "Query Options");
121 buffer_json_member_add_string(
122 buffer, "description",
123 "Space-separated list of additional query options:\n"
124 "'percentage': Return values as percentages of total\n"
125 "'absolute' or 'absolute-sum': Return absolute values for stacked charts\n"
126 "'display-absolute': Convert percentage values to absolute before application of grouping functions\n"
127 "'all-dimensions': Include all dimensions, even those with just zero values\n"
128 "Example: 'absolute percentage'");
129 }
130 buffer_json_object_close(buffer); // options
131
132 // Time grouping
133 buffer_json_member_add_object(buffer, "time_group");
134 {
135 buffer_json_member_add_string(buffer, "type", "string");
136 buffer_json_member_add_string(buffer, "title", "Time Grouping Method");
137 buffer_json_member_add_string(buffer, "description", "Method to group data points over time. The 'extremes' method returns the maximum value for positive numbers and the minimum value for negative numbers, which is particularly useful for showing the highest peaks in both directions on charts.");
138 buffer_json_member_add_string(buffer, "default", "average");
139
140 // Define enum of possible values
141 buffer_json_member_add_array(buffer, "enum");
142 buffer_json_add_array_item_string(buffer, "average"); // "avg" and "mean" are aliases
143 buffer_json_add_array_item_string(buffer, "min");
144 buffer_json_add_array_item_string(buffer, "max");
145 buffer_json_add_array_item_string(buffer, "sum");
146 buffer_json_add_array_item_string(buffer, "incremental-sum"); // "incremental_sum" is an alias
147 buffer_json_add_array_item_string(buffer, "median");
148 buffer_json_add_array_item_string(buffer, "trimmed-mean");
149 buffer_json_add_array_item_string(buffer, "trimmed-median");
150 buffer_json_add_array_item_string(buffer, "percentile"); // requires time_group_options parameter
151 buffer_json_add_array_item_string(buffer, "stddev"); // standard deviation
152 buffer_json_add_array_item_string(buffer, "coefficient-of-variation"); // relative standard deviation (cv)
153 buffer_json_add_array_item_string(buffer, "ema"); // exponential moving average (alias "ses" or "ewma")
154 buffer_json_add_array_item_string(buffer, "des"); // double exponential smoothing
155 buffer_json_add_array_item_string(buffer, "countif"); // requires time_group_options parameter
156 buffer_json_add_array_item_string(buffer, "extremes"); // for each time frame, returns max for positive values and min for negative values
157 buffer_json_array_close(buffer);
158 }
159 buffer_json_object_close(buffer); // time_group
160
161 buffer_json_member_add_object(buffer, "time_group_options");
162 {
163 buffer_json_member_add_string(buffer, "type", "string");
164 buffer_json_member_add_string(buffer, "title", "Time Group Options");
165 buffer_json_member_add_string(
166 buffer, "description",
167 "Additional options for time grouping.\n"
168 "For 'percentile', specify a percentage (0-100).\n"
169 "For 'countif', specify a comparison operator and value (e.g., '>0', '=0', '!=0', '<=10').");
170 }
171 buffer_json_object_close(buffer); // time_group_options
172
173 // Tier selection
174 buffer_json_member_add_object(buffer, "tier");
175 {
176 buffer_json_member_add_string(buffer, "type", "number");
177 buffer_json_member_add_string(buffer, "title", "Storage Tier");
178 buffer_json_member_add_string(
179 buffer, "description",
180 "Storage tier to query from.\n"
181 "If not specified, Netdata will automatically pick the best tier based on the time-frame and points requested.\n"
182 "CAUTION: specifying a high-resolution tier (like 0) over long time-frames (like days) may consume significant system resources.");
183 }
184 buffer_json_object_close(buffer); // tier
185
186 // Group by parameters
187 buffer_json_member_add_object(buffer, "group_by");
188 {
189 buffer_json_member_add_string(buffer, "type", "array");
190 buffer_json_member_add_string(buffer, "title", "Group By");
191 buffer_json_member_add_string(
192 buffer, "description",
193 "Specifies how to group metrics across different time-series.\n"
194 "- 'dimension': Groups by dimension name across all instances/nodes. Example: for disks it provides the aggregate of reads and writes across all disks of all nodes.\n"
195 "- 'instance': Groups by instance across all nodes. Example: for disks, it provides the aggregate per disk name (sda, sdb, etc), aggregating their reads and writes, across all nodes.\n"
196 "- 'node': Groups by node. Example: for disks, it provides one metric per node, aggregating reads and writes across all its disks.\n"
197 "- 'label': Groups by the given label key (use the parameter 'group_by_label' to set the key). Example: for disks, aggregate over key 'disk_type' to get an group all 'physical', 'virtual' and 'partition' separately.\n"
198 "Multiple groupings can be combined. Example: '[\"dimension\", \"label\"]'.");
199 buffer_json_member_add_array(buffer, "default");
200 buffer_json_add_array_item_string(buffer, "dimension");
201 buffer_json_array_close(buffer);
202
203 // Define items schema with enum values
204 buffer_json_member_add_object(buffer, "items");
205 {
206 buffer_json_member_add_string(buffer, "type", "string");
207 buffer_json_member_add_array(buffer, "enum");
208 buffer_json_add_array_item_string(buffer, "dimension");
209 buffer_json_add_array_item_string(buffer, "instance");
210 buffer_json_add_array_item_string(buffer, "node");
211 buffer_json_add_array_item_string(buffer, "label");
212
213 // we don't offer these to MCP clients.
214 // buffer_json_add_array_item_string(buffer, "context");
215 // buffer_json_add_array_item_string(buffer, "units");
216 buffer_json_array_close(buffer);
217 }
218 buffer_json_object_close(buffer); // items
219 }
220 buffer_json_object_close(buffer); // group_by
221
222 mcp_schema_add_string_param(
223 buffer, "group_by_label",
224 "Group By Label",
225 "When 'group_by' includes 'label', this parameter specifies the label key to group by.\n"
226 "Example: if metrics have an 'interface_type' label with values like 'real' or 'virtual', "
227 "setting 'group_by_label' to 'interface_type' would aggregate metrics separately for physical and virtual network interfaces.",
228 NULL, false);
229
230 buffer_json_member_add_object(buffer, "aggregation");
231 {
232 buffer_json_member_add_string(buffer, "type", "string");
233 buffer_json_member_add_string(buffer, "title", "Aggregation Method");
234 buffer_json_member_add_string(
235 buffer, "description",
236 "Method to use when aggregating grouped metrics.\n"
237 "- 'sum': Sum of all grouped metrics (useful for additive metrics like bytes transferred, operations, etc.)\n"
238 "- 'min': Minimum value among all grouped metrics (useful for finding best performance metrics)\n"
239 "- 'max': Maximum value among all grouped metrics (useful for finding worst performance metrics, peak resource usage)\n"
240 "- 'extremes': When values are both positive and negative, shows the maximum value for positive metrics and the minimum value for negative metrics\n"
241 "- 'average': Average of all grouped metrics (CAUTION: When 'group_by' doesn't include 'dimension', this averages different metric types together - e.g., CPU user + system + idle - which is rarely meaningful)\n"
242 "- 'percentage': Expresses each grouped metric as a percentage of its group's total (useful for seeing proportional contributions)\n");
243
244 // Define enum of possible values
245 buffer_json_member_add_array(buffer, "enum");
246 buffer_json_add_array_item_string(buffer, "sum");
247 buffer_json_add_array_item_string(buffer, "min");
248 buffer_json_add_array_item_string(buffer, "max");
249 buffer_json_add_array_item_string(buffer, "extremes");
250 buffer_json_add_array_item_string(buffer, "average");
251 buffer_json_add_array_item_string(buffer, "percentage");
252 buffer_json_array_close(buffer);
253 }
254 buffer_json_object_close(buffer); // aggregation
255
256 buffer_json_object_close(buffer); // properties
257
258 // Required fields
259 buffer_json_member_add_array(buffer, "required");
260 buffer_json_add_array_item_string(buffer, "metric");
261 buffer_json_add_array_item_string(buffer, "dimensions");
262 buffer_json_add_array_item_string(buffer, "after");
263 buffer_json_add_array_item_string(buffer, "before");
264 buffer_json_add_array_item_string(buffer, "points");
265 buffer_json_add_array_item_string(buffer, "time_group");
266 buffer_json_add_array_item_string(buffer, "group_by");
267 buffer_json_add_array_item_string(buffer, "aggregation");
268 buffer_json_add_array_item_string(buffer, "cardinality_limit");
269 buffer_json_array_close(buffer);
270
271 buffer_json_object_close(buffer); // inputSchema
272 }
273
274 // Structure to hold interruption data
275 typedef struct {
276 MCP_CLIENT *mcpc;
277 MCP_REQUEST_ID id;
278 } mcp_query_interrupt_data;
279
280 // Interrupt callback for query execution
281 static bool mcp_query_interrupt_callback(void *data) {
282 (void)data;
283
284 // Real implementations might check for client disconnection or timeout
285 // Here we're just returning false to indicate "no interrupt"
286 return false;
287 }
288
289 // Removed extract_string_param and extract_size_param - now using mcp-params functions
290
291 // Execute the metrics query
292 MCP_RETURN_CODE mcp_tool_query_metrics_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
293 if (!mcpc)
294 return MCP_RC_ERROR;
295
296 usec_t received_ut = now_monotonic_usec();
297
298 // Extract and validate context parameter
299 const char *context = mcp_params_extract_string(params, "metric", NULL);
300
301 // Validate required parameters with detailed error messages
302 if (!context || !*context) {
303 buffer_sprintf(mcpc->error, "Missing required parameter 'metric'. Use the '" MCP_TOOL_LIST_METRICS "' tool to discover available metrics/contexts.");
304 return MCP_RC_BAD_REQUEST;
305 }
306
307 // Check if context contains patterns
308 if (simple_pattern_contains_wildcards(context, SIMPLE_PATTERN_DEFAULT_WEB_SEPARATORS)) {
309 buffer_sprintf(mcpc->error, "The 'context' parameter must be an exact context name, not a pattern. "
310 "Wildcards or pattern separators are not supported. "
311 "Use the " MCP_TOOL_LIST_METRICS " tool to discover exact context names.");
312 return MCP_RC_BAD_REQUEST;
313 }
314
315 // Check if all required parameters are provided
316 struct json_object *after_obj = NULL, *before_obj = NULL, *points_obj = NULL, *time_group_obj = NULL;
317 struct json_object *group_by_obj = NULL, *aggregation_obj = NULL, *dimensions_obj = NULL;
318
319 if (!json_object_object_get_ex(params, "dimensions", &dimensions_obj) || !dimensions_obj) {
320 buffer_sprintf(mcpc->error, "Missing required parameter 'dimensions'. Use the '" MCP_TOOL_LIST_METRICS "' to get the list of dimensions for this metric/context.");
321 return MCP_RC_BAD_REQUEST;
322 }
323
324 if (!json_object_object_get_ex(params, "after", &after_obj) || !after_obj) {
325 buffer_sprintf(mcpc->error, "Missing required parameter 'after'. This parameter defines the start time for your query (Unix epoch timestamp in seconds, or negative value relative to 'before', or RFC3339 datetime string).");
326 return MCP_RC_BAD_REQUEST;
327 }
328
329 if (!json_object_object_get_ex(params, "before", &before_obj) || !before_obj) {
330 buffer_sprintf(mcpc->error, "Missing required parameter 'before'. This parameter defines the end time for your query (Unix epoch timestamp in seconds, or negative value relative to now, or RFC3339 datetime string).");
331 return MCP_RC_BAD_REQUEST;
332 }
333
334 if (!json_object_object_get_ex(params, "points", &points_obj) || !points_obj) {
335 buffer_sprintf(mcpc->error, "Missing required parameter 'points'. This parameter defines how many data points to return in your result set (e.g., 60 for minute-level granularity in an hour).");
336 return MCP_RC_BAD_REQUEST;
337 }
338
339 if (!json_object_object_get_ex(params, "time_group", &time_group_obj) || !time_group_obj) {
340 buffer_sprintf(mcpc->error, "Missing required parameter 'time_group'. This parameter defines how to aggregate data points over time (e.g., 'average', 'min', 'max', 'sum').");
341 return MCP_RC_BAD_REQUEST;
342 }
343
344 if (!json_object_object_get_ex(params, "group_by", &group_by_obj) || !group_by_obj) {
345 buffer_sprintf(mcpc->error, "Missing required parameter 'group_by'. This parameter defines how to group metrics (e.g., 'dimension', 'instance', 'node', or combinations like 'dimension,node').");
346 return MCP_RC_BAD_REQUEST;
347 }
348
349 if (!json_object_object_get_ex(params, "aggregation", &aggregation_obj) || !aggregation_obj) {
350 buffer_sprintf(mcpc->error, "Missing required parameter 'aggregation'. This parameter defines the function to use when aggregating metrics (e.g., 'sum', 'min', 'max', 'average').");
351 return MCP_RC_BAD_REQUEST;
352 }
353
354 struct json_object *cardinality_limit_obj = NULL;
355 if (!json_object_object_get_ex(params, "cardinality_limit", &cardinality_limit_obj) || !cardinality_limit_obj) {
356 buffer_sprintf(mcpc->error, "Missing required parameter 'cardinality_limit'. This parameter limits the number of items returned to keep response sizes manageable (default: %d).", MCP_DATA_CARDINALITY_LIMIT);
357 return MCP_RC_BAD_REQUEST;
358 }
359
360 // Get time_group value to check if it's a percentile or countif
361 const char *time_group_str = NULL;
362 if (json_object_is_type(time_group_obj, json_type_string)) {
363 time_group_str = json_object_get_string(time_group_obj);
364
365 // Check if time_group_options is required based on time_group
366 if (time_group_str && (
367 strcmp(time_group_str, "percentile") == 0 ||
368 strcmp(time_group_str, "countif") == 0)) {
369
370 struct json_object *time_group_options_obj = NULL;
371 if (!json_object_object_get_ex(params, "time_group_options", &time_group_options_obj) || !time_group_options_obj) {
372 if (strcmp(time_group_str, "percentile") == 0) {
373 buffer_sprintf(mcpc->error, "Missing required parameter 'time_group_options' when using time_group='percentile'. You must specify a percentage value between 0-100 (e.g., '95' for 95th percentile).");
374 } else {
375 buffer_sprintf(mcpc->error, "Missing required parameter 'time_group_options' when using time_group='countif'. You must specify a comparison operator and value (e.g., '>0', '=0', '!=0', '<=10').");
376 }
377 return MCP_RC_BAD_REQUEST;
378 }
379 }
380 }
381
382 // Handle nodes array parameter
383 CLEAN_BUFFER *nodes_buffer = NULL;
384
385 nodes_buffer = mcp_params_parse_array_to_pattern(params, "nodes", false, false, MCP_TOOL_LIST_NODES, mcpc->error);
386 if (buffer_strlen(mcpc->error) > 0) {
387 return MCP_RC_BAD_REQUEST;
388 }
389
390 // Handle instances array parameter
391 CLEAN_BUFFER *instances_buffer = NULL;
392
393 instances_buffer = mcp_params_parse_array_to_pattern(params, "instances", false, false, MCP_TOOL_GET_METRICS_DETAILS, mcpc->error);
394 if (buffer_strlen(mcpc->error) > 0) {
395 return MCP_RC_BAD_REQUEST;
396 }
397
398 // Handle dimensions array parameter
399 CLEAN_BUFFER *dimensions_buffer = NULL;
400
401 dimensions_buffer = mcp_params_parse_array_to_pattern(params, "dimensions", true, false, MCP_TOOL_GET_METRICS_DETAILS, mcpc->error);
402 if (buffer_strlen(mcpc->error) > 0) {
403 buffer_strcat(mcpc->error, ". You must explicitly list every dimension you want to query. "
404 "Use the '" MCP_TOOL_GET_METRICS_DETAILS "' tool to discover available dimensions for the context.");
405 return MCP_RC_BAD_REQUEST;
406 }
407 // Handle labels - expects a structured object only
408 CLEAN_BUFFER *labels_buffer = NULL;
409
410 labels_buffer = mcp_params_parse_labels_object(params, MCP_TOOL_GET_METRICS_DETAILS, mcpc->error);
411 if (buffer_strlen(mcpc->error) > 0) {
412 return MCP_RC_BAD_REQUEST;
413 }
414
415 // Removed alerts parameter - not used in query_metrics
416
417 // Time parameters - parse and validate together
418 time_t after, before;
419 if (!mcp_params_parse_time_window(params, &after, &before,
420 MCP_DEFAULT_AFTER_TIME, MCP_DEFAULT_BEFORE_TIME,
421 false, mcpc->error)) {
422 return MCP_RC_BAD_REQUEST;
423 }
424
425 // No need to check aggregation_obj here - we already have a default in group_by struct
426
427 // Other parameters
428 size_t points = mcp_params_extract_size(params, "points", 0, 0, SIZE_MAX, mcpc->error);
429 if (buffer_strlen(mcpc->error) > 0) {
430 return MCP_RC_BAD_REQUEST;
431 }
432
433 size_t cardinality_limit = mcp_params_extract_size(params, "cardinality_limit", MCP_DATA_CARDINALITY_LIMIT, 1, 1000, mcpc->error);
434 if (buffer_strlen(mcpc->error) > 0) {
435 return MCP_RC_BAD_REQUEST;
436 }
437
438 // Check if points is more than 1000
439 if (points < 1) {
440 buffer_sprintf(mcpc->error,
441 "Too few data points requested: %zu. The minimum allowed is 1 point.",
442 points);
443 return MCP_RC_BAD_REQUEST;
444 }
445
446 // Check if points is more than 1000
447 if (points > 1000) {
448 buffer_sprintf(mcpc->error,
449 "Too many data points requested: %zu. The maximum allowed is 1000 points. Please reduce the 'points' parameter value to 1000 or less.\n"
450 "This limit helps reduce response size and save context space when used with AI assistants.",
451 points);
452 return MCP_RC_BAD_REQUEST;
453 }
454
455 long timeout = mcp_params_extract_timeout(params, "timeout", 0, 0, 3600, mcpc->error);
456 if (buffer_strlen(mcpc->error) > 0) {
457 return MCP_RC_BAD_REQUEST;
458 }
459
460 const char *options_str = mcp_params_extract_string(params, "options", NULL);
461 RRDR_OPTIONS options = 0;
462 if (options_str && *options_str)
463 options |= rrdr_options_parse(options_str);
464
465 // Time grouping
466 RRDR_TIME_GROUPING time_group = RRDR_GROUPING_AVERAGE;
467 if (time_group_str && *time_group_str)
468 time_group = time_grouping_parse(time_group_str, RRDR_GROUPING_AVERAGE);
469
470 const char *time_group_options = mcp_params_extract_string(params, "time_group_options", NULL);
471
472 // Tier selection (give an invalid default to now the caller added a tier to the query)
473 size_t tier = mcp_params_extract_size(params, "tier", nd_profile.storage_tiers + 1, 0, SIZE_MAX, mcpc->error);
474 if (buffer_strlen(mcpc->error) > 0) {
475 return MCP_RC_BAD_REQUEST;
476 }
477 if (tier < nd_profile.storage_tiers)
478 options |= RRDR_OPTION_SELECTED_TIER;
479 else
480 tier = 0;
481
482 // Group by parameters (simplified - in real implementation handle multiple passes)
483 struct group_by_pass group_by[MAX_QUERY_GROUP_BY_PASSES] = {
484 {
485 .group_by = RRDR_GROUP_BY_NONE,
486 .group_by_label = NULL,
487 .aggregation = RRDR_GROUP_BY_FUNCTION_AVERAGE,
488 },
489 };
490
491 // Handle group_by array parameter and convert to comma-separated string
492 CLEAN_BUFFER *group_by_buffer = NULL;
493 const char *group_by_str = NULL;
494
495 group_by_buffer = mcp_params_parse_array_to_pattern(params, "group_by", true, false, MCP_TOOL_GET_METRICS_DETAILS, mcpc->error);
496 if (buffer_strlen(mcpc->error) > 0) {
497 return MCP_RC_BAD_REQUEST;
498 }
499
500 if (group_by_buffer && buffer_strlen(group_by_buffer) > 0) {
501 group_by_str = buffer_tostring(group_by_buffer);
502 group_by[0].group_by = group_by_parse(group_by_str);
503 }
504
505 const char *group_by_label = mcp_params_extract_string(params, "group_by_label", NULL);
506 if (group_by_label && *group_by_label) {
507 group_by[0].group_by_label = (char *)group_by_label;
508 group_by[0].group_by |= RRDR_GROUP_BY_LABEL;
509 }
510
511 const char *aggregation_str = mcp_params_extract_string(params, "aggregation", NULL);
512 if (aggregation_str && *aggregation_str)
513 group_by[0].aggregation = group_by_aggregate_function_parse(aggregation_str);
514
515 // Create interrupt callback data
516 mcp_query_interrupt_data interrupt_data = {
517 .mcpc = mcpc,
518 .id = id
519 };
520
521 // Prepare a query target request
522 QUERY_TARGET_REQUEST qtr = {
523 .version = 3,
524 .scope_nodes = buffer_tostring(nodes_buffer), // Use nodes as scope_nodes
525 .scope_contexts = context, // Use the single context as scope_contexts
526 .scope_instances = buffer_tostring(instances_buffer), // Use instances as scope_instances for MCP
527 .scope_labels = buffer_tostring(labels_buffer), // Use labels as scope_labels for MCP
528 .scope_dimensions = buffer_tostring(dimensions_buffer), // Use dimensions as scope_dimensions for MCP
529 .after = after,
530 .before = before,
531 .host = NULL,
532 .st = NULL,
533 .nodes = NULL, // Don't use the 'nodes' parameter here (we use scope_nodes)
534 .contexts = NULL, // Don't use the 'contexts' parameter here (we use scope_contexts)
535 .instances = NULL, // Don't use the 'instances' parameter here (we use scope_instances)
536 .dimensions = NULL, // Don't use the 'dimensions' parameter here (we use scope_dimensions)
537 .alerts = NULL,
538 .timeout_ms = (int)(timeout * MSEC_PER_SEC),
539 .points = points,
540 .format = DATASOURCE_JSON2,
541 .options = options |
542 RRDR_OPTION_ABSOLUTE | RRDR_OPTION_JSON_WRAP | RRDR_OPTION_RETURN_JWAR |
543 RRDR_OPTION_VIRTUAL_POINTS | RRDR_OPTION_NOT_ALIGNED | RRDR_OPTION_NONZERO |
544 RRDR_OPTION_MINIFY | RRDR_OPTION_MINIMAL_STATS | RRDR_OPTION_LONG_JSON_KEYS |
545 RRDR_OPTION_MCP_INFO | RRDR_OPTION_RFC3339,
546 .time_group_method = time_group,
547 .time_group_options = time_group_options,
548 .resampling_time = 0,
549 .tier = tier,
550 .chart_label_key = NULL,
551 .labels = NULL, // Don't use labels parameter here (we use scope_labels)
552 .query_source = QUERY_SOURCE_API_DATA,
553 .priority = STORAGE_PRIORITY_NORMAL,
554 .received_ut = received_ut,
555 .cardinality_limit = cardinality_limit,
556
557 .interrupt_callback = mcp_query_interrupt_callback,
558 .interrupt_callback_data = &interrupt_data,
559
560 .transaction = NULL, // No transaction for MCP
561 };
562
563 // Copy group_by structures
564 for (size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES; g++)
565 qtr.group_by[g] = group_by[g];
566
567 // Create a query target
568 QUERY_TARGET *qt = query_target_create(&qtr);
569 if (!qt) {
570 buffer_sprintf(mcpc->error, "Failed to prepare the query.");
571 return MCP_RC_INTERNAL_ERROR;
572 }
573
574 // Create a temporary buffer for the query result
575 CLEAN_BUFFER *tmp_buffer = buffer_create(0, NULL);
576
577 // Prepare onewayalloc for query execution
578 ONEWAYALLOC *owa = onewayalloc_create(0);
579
580 // Execute the query and get the data
581 time_t latest_timestamp = 0;
582 int ret = data_query_execute(owa, tmp_buffer, qt, &latest_timestamp);
583
584 // Clean up
585 query_target_release(qt);
586 onewayalloc_destroy(owa);
587
588 if (ret != HTTP_RESP_OK) {
589 const char *error_desc = "unknown error";
590
591 // Map common HTTP error codes to more descriptive messages
592 switch (ret) {
593 case HTTP_RESP_BAD_REQUEST:
594 error_desc = "bad request parameters";
595 break;
596 case HTTP_RESP_NOT_FOUND:
597 error_desc = "metric/context not found";
598 break;
599 case HTTP_RESP_GATEWAY_TIMEOUT:
600 case HTTP_RESP_SERVICE_UNAVAILABLE:
601 error_desc = "timeout or service unavailable";
602 break;
603 case HTTP_RESP_INTERNAL_SERVER_ERROR:
604 error_desc = "internal server error";
605 break;
606 default:
607 break;
608 }
609
610 buffer_sprintf(mcpc->error, "Failed to execute query: %s (http error code: %d). The context '%s' might not exist, or no data is available for the specified time range.",
611 error_desc, ret, context);
612 return MCP_RC_INTERNAL_ERROR;
613 }
614
615 // Check if instance filtering or grouping is used
616 bool using_instances = (instances_buffer && buffer_strlen(instances_buffer) > 0) ||
617 (group_by[0].group_by & RRDR_GROUP_BY_INSTANCE);
618
619 // Return the raw query engine response as-is
620 mcp_init_success_result(mcpc, id);
621 {
622 buffer_json_member_add_array(mcpc->result, "content");
623 {
624 // Main result content
625 buffer_json_add_array_item_object(mcpc->result);
626 {
627 buffer_json_member_add_string(mcpc->result, "type", "text");
628 buffer_json_member_add_string(mcpc->result, "text", buffer_tostring(tmp_buffer));
629 }
630 buffer_json_object_close(mcpc->result);
631
632 // Add a warning about potentially misleading aggregation
633 bool warn_aggregation = false;
634 // Only warn if using average without dimension grouping AND multiple dimensions selected
635 int dimensions_count = (int)json_object_array_length(dimensions_obj);
636 if (dimensions_count > 1 &&
637 group_by[0].aggregation == RRDR_GROUP_BY_FUNCTION_AVERAGE &&
638 !(group_by[0].group_by & RRDR_GROUP_BY_DIMENSION)) {
639 warn_aggregation = true;
640 }
641
642 if (warn_aggregation) {
643 buffer_json_add_array_item_object(mcpc->result);
644 {
645 buffer_json_member_add_string(mcpc->result, "type", "text");
646 buffer_json_member_add_string(mcpc->result, "text",
647 "⚠️ WARNING: Potentially Misleading Aggregation\n\n"
648 "You are using 'average' aggregation without including 'dimension' in group_by. "
649 "This means different metric types are being averaged together, which rarely produces meaningful results.\n\n"
650 "For example:\n"
651 "- For CPU metrics: averaging user, system, idle, wait states together\n"
652 "- For network metrics: averaging in/out traffic together\n"
653 "- For disk I/O: averaging reads and writes together\n\n"
654 "Check the 'aggregated' field in view.dimensions to see how many time-series were combined. "
655 "Values greater than 1 indicate multiple different metrics were averaged together.\n\n"
656 "Consider using:\n"
657 "- 'sum' aggregation for additive metrics\n"
658 "- Include 'dimension' in group_by (e.g., 'instance,dimension')\n"
659 "- Review the summary section to understand what's being aggregated");
660 }
661 buffer_json_object_close(mcpc->result);
662 }
663
664 // Add an instance usage warning if applicable
665 if (using_instances) {
666 buffer_json_add_array_item_object(mcpc->result);
667 {
668 buffer_json_member_add_string(mcpc->result, "type", "text");
669 buffer_json_member_add_string(mcpc->result, "text",
670 "⚠️ Instance Usage Notice: Instance filtering/grouping behavior varies by collector type:\n\n"
671 "- **Stable instances** (systemd services, cgroups): Instance names are typically stable and match their labels. "
672 "Filtering by instance works reliably.\n\n"
673 "- **Dynamic instances** (Kubernetes pods, containers, processes): Instance names often contain random IDs or session identifiers. "
674 "Each restart creates a new instance. For these, filtering/grouping by labels is recommended to see the complete picture across all instances.\n\n"
675 "- **Detecting restarts**: Grouping by labels and examining instance counts can reveal restart patterns - "
676 "multiple instances with the same labels but different names often indicate restarts or scaling events.\n\n"
677 "Best practice: Check if your target system uses stable or dynamic instances. When in doubt, group by labels for comprehensive data, "
678 "then examine instance patterns for additional insights.");
679 }
680 buffer_json_object_close(mcpc->result);
681 }
682 }
683 buffer_json_array_close(mcpc->result); // Close content array
684 }
685 buffer_json_object_close(mcpc->result); // Close result object
686 buffer_json_finalize(mcpc->result); // Finalize the JSON
687
688 return MCP_RC_OK;
689 }