This is an automated email from the ASF dual-hosted git repository. zhouzixin pushed a commit to branch add-mqe in repository https://gitbox.apache.org/repos/asf/skywalking-mcp.git
commit b2c8b6928596ac7b18a8072fd47e455649fc1c1e Author: Zixin Zhou <[email protected]> AuthorDate: Mon Jul 7 23:32:40 2025 +0800 add support for MQE --- README.md | 3 + internal/swmcp/server.go | 15 +- internal/tools/common.go | 77 +++++- internal/tools/mqe.go | 595 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 675 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8a120b8..a07b904 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,9 @@ SkyWalking MCP provides the following tools to query and analyze SkyWalking OAP | **Metrics** | `query_single_metrics` | Query single metric values | Get specific metric values (CPM, response time, SLA, Apdex); Multiple entity scopes (Service, ServiceInstance, Endpoint, Process, Relations); Time range and cold storage support | | **Metrics** | `query_top_n_metrics` | Query top N metric rankings | Rank entities by metric values; Configurable top N count; Ascending/descending order; Scope-based filtering; Performance analysis and issue identification | | **Log** | `query_logs` | Query logs from SkyWalking OAP | Filter by service, instance, endpoint, trace ID, tags; Time range queries; Cold storage support; Pagination support | +| **MQE** | `execute_mqe_expression` | Execute MQE expressions for metrics | Execute complex MQE (Metrics Query Expression) queries; Support calculations, aggregations, comparisons, TopN, trend analysis; Multiple result types (single value, time series, sorted list); Entity filtering and relation metrics; Debug and tracing capabilities | +| **MQE** | `list_mqe_metrics` | List available metrics for MQE | Discover available metrics for MQE queries; Filter by regex patterns; Get metric metadata (type, catalog); Support service, instance, endpoint, relation, database, and infrastructure metrics | +| **MQE** | `get_mqe_metric_type` | Get metric type information | Get detailed type information for specific metrics; Understand metric structure (regular value, labeled value, sampled record); Help with correct MQE expression syntax | ## Contact Us diff --git a/internal/swmcp/server.go b/internal/swmcp/server.go index 57e2694..e2773c2 100644 --- a/internal/swmcp/server.go +++ b/internal/swmcp/server.go @@ -22,7 +22,6 @@ import ( "fmt" "net/http" "os" - "strings" "github.com/mark3labs/mcp-go/server" "github.com/sirupsen/logrus" @@ -46,6 +45,8 @@ func newMcpServer() *server.MCPServer { tools.AddMetricsTools(mcpServer) tools.AddLogTools(mcpServer) + tools.AddMQETools(mcpServer) + return mcpServer } @@ -79,21 +80,13 @@ const ( skywalkingURLEnvVar = "SW_URL" ) -// finalizeURL ensures the URL ends with "/graphql". -func finalizeURL(urlStr string) string { - if !strings.HasSuffix(urlStr, "/graphql") { - urlStr = strings.TrimRight(urlStr, "/") + "/graphql" - } - return urlStr -} - // urlAndInsecureFromEnv extracts URL and insecure flag purely from environment variables. func urlAndInsecureFromEnv() (string, bool) { urlStr := os.Getenv(skywalkingURLEnvVar) if urlStr == "" { urlStr = config.DefaultSWURL } - return finalizeURL(urlStr), false + return tools.FinalizeURL(urlStr), false } // urlAndInsecureFromHeaders extracts URL and insecure flag for a request. @@ -108,7 +101,7 @@ func urlAndInsecureFromHeaders(req *http.Request) (string, bool) { } } - return finalizeURL(urlStr), false + return tools.FinalizeURL(urlStr), false } // WithSkyWalkingContextFromEnv injects the SkyWalking URL and insecure diff --git a/internal/tools/common.go b/internal/tools/common.go index 08475f9..617596a 100644 --- a/internal/tools/common.go +++ b/internal/tools/common.go @@ -19,6 +19,7 @@ package tools import ( "fmt" + "strings" "time" api "skywalking.apache.org/repo/goapi/query" @@ -28,8 +29,8 @@ import ( const ( DefaultPageSize = 15 DefaultPageNum = 1 - DefaultStep = "MINUTE" DefaultDuration = 30 // minutes + nowKeyword = "now" ) // Error messages @@ -38,6 +39,14 @@ const ( ErrMarshalFailed = "failed to marshal result: %v" ) +// FinalizeURL ensures the URL ends with "/graphql". +func FinalizeURL(urlStr string) string { + if !strings.HasSuffix(urlStr, "/graphql") { + urlStr = strings.TrimRight(urlStr, "/") + "/graphql" + } + return urlStr +} + // FormatTimeByStep formats time according to step granularity func FormatTimeByStep(t time.Time, step api.Step) string { switch step { @@ -105,11 +114,15 @@ func BuildDuration(start, end, step string, cold bool, defaultDurationMinutes in if start != "" || end != "" { stepEnum := api.Step(step) if step == "" || !stepEnum.IsValid() { - stepEnum = DefaultStep + stepEnum = api.StepMinute } + + // Parse and format start and end times + startTime, endTime := parseStartEndTimes(start, end) + return api.Duration{ - Start: start, - End: end, + Start: FormatTimeByStep(startTime, stepEnum), + End: FormatTimeByStep(endTime, stepEnum), Step: stepEnum, ColdStage: &cold, } @@ -168,3 +181,59 @@ func parseLegacyDuration(durationStr string) (startTime, endTime time.Time, step step = api.StepDay return startTime, endTime, step } + +// parseAbsoluteTime tries to parse absolute time in various formats +func parseAbsoluteTime(timeStr string) (time.Time, bool) { + timeFormats := []string{ + "2006-01-02 15:04:05", + "2006-01-02 15:04", + "2006-01-02 1504", + "2006-01-02 15", + "2006-01-02 150405", + "2006-01-02", + } + + for _, format := range timeFormats { + if parsed, err := time.Parse(format, timeStr); err == nil { + return parsed, true + } + } + + return time.Time{}, false +} + +// parseTimeString parses a time string (start or end) +func parseTimeString(timeStr string, defaultTime time.Time) time.Time { + now := time.Now() + + if timeStr == "" { + return defaultTime + } + + if strings.EqualFold(timeStr, nowKeyword) { + return now + } + + // Try relative time like "-30m", "-1h" + if duration, err := time.ParseDuration(timeStr); err == nil { + return now.Add(duration) + } + + // Try absolute time + if parsed, ok := parseAbsoluteTime(timeStr); ok { + return parsed + } + + return defaultTime +} + +// parseStartEndTimes parses start and end time strings +func parseStartEndTimes(start, end string) (startTime, endTime time.Time) { + now := time.Now() + defaultStart := now.Add(-30 * time.Minute) // Default to 30 minutes ago + + startTime = parseTimeString(start, defaultStart) + endTime = parseTimeString(end, now) + + return startTime, endTime +} diff --git a/internal/tools/mqe.go b/internal/tools/mqe.go new file mode 100644 index 0000000..3fd4e2d --- /dev/null +++ b/internal/tools/mqe.go @@ -0,0 +1,595 @@ +// Licensed to Apache Software Foundation (ASF) under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Apache Software Foundation (ASF) licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package tools + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/spf13/viper" + api "skywalking.apache.org/repo/goapi/query" +) + +// AddMQETools registers MQE-related tools with the MCP server +func AddMQETools(mcp *server.MCPServer) { + MQEExpressionTool.Register(mcp) + MQEMetricsListTool.Register(mcp) + MQEMetricsTypeTool.Register(mcp) +} + +// GraphQLRequest represents a GraphQL request +type GraphQLRequest struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables,omitempty"` +} + +// GraphQLResponse represents a GraphQL response +type GraphQLResponse struct { + Data interface{} `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors,omitempty"` +} + +// executeGraphQL executes a GraphQL query against SkyWalking OAP +func executeGraphQL(ctx context.Context, query string, variables map[string]interface{}) (*GraphQLResponse, error) { + url := FinalizeURL(viper.GetString("url")) + + reqBody := GraphQLRequest{ + Query: query, + Variables: variables, + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal GraphQL request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute HTTP request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("HTTP request failed with status: %d, body: %s", resp.StatusCode, string(bodyBytes)) + } + + var graphqlResp GraphQLResponse + if err := json.NewDecoder(resp.Body).Decode(&graphqlResp); err != nil { + return nil, fmt.Errorf("failed to decode GraphQL response: %w", err) + } + + if len(graphqlResp.Errors) > 0 { + var errorMsgs []string + for _, err := range graphqlResp.Errors { + errorMsgs = append(errorMsgs, err.Message) + } + return nil, fmt.Errorf("GraphQL errors: %s", strings.Join(errorMsgs, ", ")) + } + + return &graphqlResp, nil +} + +// MQEExpressionRequest represents a request to execute MQE expression +type MQEExpressionRequest struct { + Expression string `json:"expression"` + ServiceName string `json:"service_name,omitempty"` + Layer string `json:"layer,omitempty"` + ServiceInstanceName string `json:"service_instance_name,omitempty"` + EndpointName string `json:"endpoint_name,omitempty"` + ProcessName string `json:"process_name,omitempty"` + Normal *bool `json:"normal,omitempty"` + DestServiceName string `json:"dest_service_name,omitempty"` + DestLayer string `json:"dest_layer,omitempty"` + DestServiceInstanceName string `json:"dest_service_instance_name,omitempty"` + DestEndpointName string `json:"dest_endpoint_name,omitempty"` + DestProcessName string `json:"dest_process_name,omitempty"` + DestNormal *bool `json:"dest_normal,omitempty"` + Duration string `json:"duration,omitempty"` + Start string `json:"start,omitempty"` + End string `json:"end,omitempty"` + Step string `json:"step,omitempty"` + Cold bool `json:"cold,omitempty"` + Debug bool `json:"debug,omitempty"` + DumpDBRsp bool `json:"dump_db_rsp,omitempty"` +} + +// MQEMetricsListRequest represents a request to list available metrics +type MQEMetricsListRequest struct { + Regex string `json:"regex,omitempty"` +} + +// MQEMetricsTypeRequest represents a request to get metric type +type MQEMetricsTypeRequest struct { + MetricName string `json:"metric_name"` +} + +// ListServicesRequest represents a request to list services +type ListServicesRequest struct { + Layer string `json:"layer"` +} + +// getServiceInfo queries service information using the specified layer +func getServiceInfo(ctx context.Context, serviceName, layer string) bool { + if serviceName == "" { + return false + } + + if layer == "" { + layer = "GENERAL" + } + + normal, err := getServiceByName(ctx, serviceName, layer) + if err != nil { + return true + } + if normal != nil { + return *normal + } + + return true +} + +// getServiceByName tries to get service info directly by name in specified layer +func getServiceByName(ctx context.Context, serviceName, layer string) (*bool, error) { + serviceID, err := findServiceID(ctx, serviceName, layer) + if err != nil { + return nil, fmt.Errorf("service not found in layer %s: %s", layer, serviceName) + } + if serviceID == "" { + return nil, fmt.Errorf("service not found in layer %s: %s", layer, serviceName) + } + + query := ` + query getService($serviceId: String!) { + service: getService(serviceId: $serviceId) { + id + name + normal + layers + } + } + ` + + variables := map[string]interface{}{ + "serviceId": serviceID, + } + + result, err := executeGraphQL(ctx, query, variables) + if err != nil { + return nil, fmt.Errorf("failed to get service details: %w", err) + } + + if data, ok := result.Data.(map[string]interface{}); ok { + if service, ok := data["service"].(map[string]interface{}); ok { + if normal, ok := service["normal"].(bool); ok { + return &normal, nil + } + } + } + + return nil, fmt.Errorf("invalid service data returned for: %s", serviceName) +} + +// findServiceID finds service ID by name in a specific layer +func findServiceID(ctx context.Context, serviceName, layer string) (string, error) { + query := ` + query getServices($layer: String!) { + services: listServices(layer: $layer) { + id + name + } + } + ` + + variables := map[string]interface{}{ + "layer": layer, + } + + result, err := executeGraphQL(ctx, query, variables) + if err != nil { + return "", err + } + + if data, ok := result.Data.(map[string]interface{}); ok { + if services, ok := data["services"].([]interface{}); ok { + for _, s := range services { + svc, ok := s.(map[string]interface{}) + if !ok { + continue + } + if svc["name"] == serviceName { + if id, ok := svc["id"].(string); ok { + return id, nil + } + } + } + } + } + + return "", nil +} + +// buildMQEEntity builds the entity from request parameters +func buildMQEEntity(ctx context.Context, req *MQEExpressionRequest) map[string]interface{} { + entity := make(map[string]interface{}) + + if req.ServiceName != "" { + entity["serviceName"] = req.ServiceName + + if req.Normal == nil { + normal := getServiceInfo(ctx, req.ServiceName, req.Layer) + entity["normal"] = normal + } else { + entity["normal"] = *req.Normal + } + } + + if req.ServiceInstanceName != "" { + entity["serviceInstanceName"] = req.ServiceInstanceName + } + if req.EndpointName != "" { + entity["endpointName"] = req.EndpointName + } + if req.ProcessName != "" { + entity["processName"] = req.ProcessName + } + if req.DestServiceName != "" { + entity["destServiceName"] = req.DestServiceName + } + if req.DestServiceInstanceName != "" { + entity["destServiceInstanceName"] = req.DestServiceInstanceName + } + if req.DestEndpointName != "" { + entity["destEndpointName"] = req.DestEndpointName + } + if req.DestProcessName != "" { + entity["destProcessName"] = req.DestProcessName + } + + if req.ServiceName == "" && req.Normal != nil { + entity["normal"] = *req.Normal + } + if req.DestNormal != nil { + entity["destNormal"] = *req.DestNormal + } + + return entity +} + +// executeMQEExpression executes MQE expression query +func executeMQEExpression(ctx context.Context, req *MQEExpressionRequest) (*mcp.CallToolResult, error) { + if req.Expression == "" { + return mcp.NewToolResultError("expression is required"), nil + } + + entity := buildMQEEntity(ctx, req) + + var duration api.Duration + if req.Duration != "" { + duration = ParseDuration(req.Duration, req.Cold) + } else { + duration = BuildDuration(req.Start, req.End, req.Step, req.Cold, DefaultDuration) + } + + // GraphQL query for MQE expression + query := ` + query execExpression($expression: String!, $entity: Entity!, $duration: Duration!, $debug: Boolean, $dumpDBRsp: Boolean) { + execExpression(expression: $expression, entity: $entity, duration: $duration, debug: $debug, dumpDBRsp: $dumpDBRsp) { + type + error + results { + metric { + labels { + key + value + } + } + values { + id + value + traceID + owner { + scope + serviceID + serviceName + normal + serviceInstanceID + serviceInstanceName + endpointID + endpointName + } + } + } + debuggingTrace { + traceId + condition + duration + spans { + spanId + operation + msg + startTime + endTime + duration + } + } + } + } + ` + + variables := map[string]interface{}{ + "expression": req.Expression, + "entity": entity, // Always include entity, even if empty + "duration": map[string]interface{}{ + "start": duration.Start, + "end": duration.End, + "step": string(duration.Step), + }, + // Always provide debug parameters with explicit values + "debug": req.Debug, + "dumpDBRsp": req.DumpDBRsp, + } + + result, err := executeGraphQL(ctx, query, variables) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to execute MQE expression: %v", err)), nil + } + + jsonBytes, err := json.Marshal(result.Data) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + return mcp.NewToolResultText(string(jsonBytes)), nil +} + +// listMQEMetrics lists available metrics +func listMQEMetrics(ctx context.Context, req *MQEMetricsListRequest) (*mcp.CallToolResult, error) { + // GraphQL query for listing metrics + query := ` + query listMetrics($regex: String) { + listMetrics(regex: $regex) { + name + type + catalog + } + } + ` + + variables := map[string]interface{}{} + if req.Regex != "" { + variables["regex"] = req.Regex + } + + result, err := executeGraphQL(ctx, query, variables) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to list metrics: %v", err)), nil + } + + jsonBytes, err := json.Marshal(result.Data) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + return mcp.NewToolResultText(string(jsonBytes)), nil +} + +// getMQEMetricsType gets metric type information +func getMQEMetricsType(ctx context.Context, req *MQEMetricsTypeRequest) (*mcp.CallToolResult, error) { + if req.MetricName == "" { + return mcp.NewToolResultError("metric_name is required"), nil + } + + // GraphQL query for getting metric type + query := ` + query typeOfMetrics($name: String!) { + typeOfMetrics(name: $name) + } + ` + + variables := map[string]interface{}{ + "name": req.MetricName, + } + + result, err := executeGraphQL(ctx, query, variables) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to get metric type: %v", err)), nil + } + + jsonBytes, err := json.Marshal(result.Data) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + return mcp.NewToolResultText(string(jsonBytes)), nil +} + +var MQEExpressionTool = NewTool[MQEExpressionRequest, *mcp.CallToolResult]( + "execute_mqe_expression", + `Execute MQE (Metrics Query Expression) to query and calculate metrics data. + +MQE is SkyWalking's powerful query language that allows you to: +- Query metrics with labels: service_percentile{p='50,75,90,95,99'} +- Perform calculations: service_sla * 100, service_cpm / 60 +- Compare values: service_resp_time > 3000 +- Use aggregations: avg(service_cpm), sum(service_cpm), max(service_resp_time) +- Mathematical functions: round(service_cpm / 60, 2), abs(service_resp_time - 1000) +- TopN queries: top_n(service_cpm, 10, des) +- Trend analysis: increase(service_cpm, 2), rate(service_cpm, 5) +- Sort operations: sort_values(service_resp_time, 10, des) +- Baseline comparison: baseline(service_resp_time, upper) +- Relabel operations: relabels(service_percentile{p='50,75,90,95,99'}, p='50,75,90,95,99', percentile='P50,P75,P90,P95,P99') +- Logical operations: view_as_seq([metric1, metric2]), is_present([metric1, metric2]) +- Label aggregation: aggregate_labels(total_commands_rate, sum) + +Result Types: +- SINGLE_VALUE: Single metric value (e.g., avg(), sum()) +- TIME_SERIES_VALUES: Time series data with timestamps +- SORTED_LIST: Sorted metric values (e.g., top_n()) +- RECORD_LIST: Record-based metrics +- LABELED_VALUE: Metrics with multiple labels + +USAGE REQUIREMENTS: +- The 'expression' parameter is mandatory for all queries +- For service-specific queries, specify 'service_name' and optionally 'layer' (defaults to GENERAL) +- For relation metrics, provide both source and destination entity parameters +- Either specify 'duration' OR both 'start' and 'end' for time range +- Use 'debug: true' for query tracing and troubleshooting +- Use 'cold: true' to query from cold storage (BanyanDB only) + +Entity Filtering (all optional): +- Service level: service_name + layer + normal +- Instance level: service_instance_name +- Endpoint level: endpoint_name +- Process level: process_name +- Relation queries: dest_service_name + dest_layer, dest_service_instance_name, etc. + +Examples: +- {expression: "service_sla * 100", service_name: "Your_ApplicationName", layer: "GENERAL", duration: "1h"}: Convert SLA to percentage for last hour +- {expression: "service_resp_time > 3000 && service_cpm < 1000", service_name: "Your_ApplicationName", + duration: "30m"}: Find high latency with low traffic +- {expression: "avg(service_cpm)", duration: "2h"}: Calculate average CPM for last 2 hours +- {expression: "top_n(service_cpm, 10, des)", start: "2025-07-06 16:00:00", end: "2025-07-06 17:00:00", + step: "MINUTE"}: Top 10 services by CPM with minute granularity`, + executeMQEExpression, + mcp.WithString("expression", mcp.Required(), + mcp.Description("MQE expression to execute (required). "+ + "Examples: `service_sla`, `avg(service_cpm)`, `service_sla * 100`, `service_percentile{p='50,75,90,95,99'}`")), + mcp.WithString("service_name", mcp.Description("Service name for entity filtering")), + mcp.WithString("layer", + mcp.Description("Service layer for entity filtering. "+ + "Examples: `GENERAL` (default), `MESH`, `K8S_SERVICE`, `DATABASE`, `VIRTUAL_DATABASE`. "+ + "Defaults to GENERAL if not specified")), + mcp.WithString("service_instance_name", mcp.Description("Service instance name for entity filtering")), + mcp.WithString("endpoint_name", mcp.Description("Endpoint name for entity filtering")), + mcp.WithString("process_name", mcp.Description("Process name for entity filtering")), + mcp.WithBoolean("normal", + mcp.Description("Whether the service is normal (has agent installed). "+ + "If not specified, will be auto-detected based on service layer")), + mcp.WithString("dest_service_name", mcp.Description("Destination service name for relation metrics")), + mcp.WithString("dest_layer", + mcp.Description("Destination service layer for relation metrics. "+ + "Examples: `GENERAL`, `MESH`, `K8S_SERVICE`, `DATABASE`")), + mcp.WithString("dest_service_instance_name", mcp.Description("Destination service instance name for relation metrics")), + mcp.WithString("dest_endpoint_name", mcp.Description("Destination endpoint name for relation metrics")), + mcp.WithString("dest_process_name", mcp.Description("Destination process name for relation metrics")), + mcp.WithBoolean("dest_normal", mcp.Description("Whether the destination service is normal")), + mcp.WithString("duration", + mcp.Description("Time duration for the query. "+ + "Examples: `1h` (last 1 hour), `30m` (last 30 minutes), `7d` (last 7 days). "+ + "Use this OR specify both start+end")), + mcp.WithString("start", mcp.Description("Start time for the query. Examples: `2025-07-06 12:00:00`, `-1h` (1 hour ago), `-30m` (30 minutes ago)")), + mcp.WithString("end", mcp.Description("End time for the query. Examples: `2025-07-06 13:00:00`, `now`, `-10m` (10 minutes ago)")), + mcp.WithString("step", mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY", "MONTH"), + mcp.Description("Time step between start time and end time: "+ + "SECOND (second-level), MINUTE (minute-level, default), HOUR (hour-level), "+ + "DAY (day-level), MONTH (month-level)")), + mcp.WithBoolean("cold", mcp.Description("Whether to query from cold-stage storage")), + mcp.WithBoolean("debug", mcp.Description("Enable query tracing and debugging")), + mcp.WithBoolean("dump_db_rsp", mcp.Description("Dump database response for debugging")), +) + +var MQEMetricsListTool = NewTool[MQEMetricsListRequest, *mcp.CallToolResult]( + "list_mqe_metrics", + `List available metrics in SkyWalking that can be used in MQE expressions. + +This tool helps you discover what metrics are available for querying and their metadata information +including metric type and catalog. You can optionally provide a regex pattern to filter the metrics by name. + +Metric Categories: +- Service metrics: service_sla, service_cpm, service_resp_time, service_apdex, service_percentile +- Instance metrics: service_instance_sla, service_instance_cpm, service_instance_resp_time +- Endpoint metrics: endpoint_sla, endpoint_cpm, endpoint_resp_time, endpoint_percentile +- Relation metrics: service_relation_client_cpm, service_relation_server_cpm +- Database metrics: database_access_resp_time, database_access_cpm +- Infrastructure metrics: service_cpu, service_memory, service_thread_count + +Metric Types: +- REGULAR_VALUE: Single value metrics (e.g., service_sla, service_cpm) +- LABELED_VALUE: Multi-label metrics (e.g., service_percentile, k8s_cluster_deployment_status) +- SAMPLED_RECORD: Record-based metrics + +Usage Tips: +- Use regex patterns to filter specific metric categories +- Check metric type to understand how to use them in MQE expressions +- Regular value metrics can be used directly in calculations +- Labeled value metrics require label selectors: metric_name{label='value'} + +Examples: +- {regex: "service_.*"}: List all service-related metrics +- {regex: ".*_cpm"}: List all CPM (calls per minute) metrics +- {regex: ".*percentile.*"}: List all percentile metrics +- {}: List all available metrics`, + listMQEMetrics, + mcp.WithString("regex", mcp.Description("Optional regex pattern to filter metrics by name. Examples: `service_.*`, `.*_cpm`, `endpoint_.*`")), +) + +var MQEMetricsTypeTool = NewTool[MQEMetricsTypeRequest, *mcp.CallToolResult]( + "get_mqe_metric_type", + `Get type information for a specific metric. + +This tool returns the type and catalog information for a given metric name, which helps understand +what kind of data the metric contains and how it should be used in MQE expressions. + +Metric Types: +- REGULAR_VALUE: Single numeric value metrics + - Can be used directly in arithmetic operations + - Examples: service_sla, service_cpm, service_resp_time + - Usage: service_sla, service_sla * 100, avg(service_cpm) + +- LABELED_VALUE: Multi-dimensional metrics with labels + - Require label selectors to specify which values to query + - Examples: service_percentile, k8s_cluster_deployment_status + - Usage: service_percentile{p='50,75,90,95,99'} + +- SAMPLED_RECORD: Record-based metrics with sampling + - Used for detailed record analysis + - Examples: top_n_database_statement, traces + - Usage: Complex aggregations and filtering + +Understanding metric types is crucial for: +- Writing correct MQE expressions +- Knowing whether to use label selectors +- Understanding result data structure +- Choosing appropriate aggregation functions + +Examples: +- {metric_name: "service_cpm"}: Get type info for service CPM metric +- {metric_name: "service_percentile"}: Get type info for service percentile metric +- {metric_name: "endpoint_sla"}: Get type info for endpoint SLA metric`, + getMQEMetricsType, + mcp.WithString("metric_name", mcp.Required(), + mcp.Description("Name of the metric to get type information for (required). "+ + "Examples: `service_sla`, `service_percentile`, `endpoint_cpm`")), +)
