From 607435faaa6974a45175c6b8aa9e84c0dabf949c Mon Sep 17 00:00:00 2001 From: josunect Date: Wed, 19 Nov 2025 11:34:46 +0000 Subject: [PATCH 1/4] Add Kiali trace details Signed-off-by: josunect --- pkg/kiali/mesh_test.go | 26 +++ pkg/kiali/traces.go | 13 ++ pkg/toolsets/kiali/traces.go | 326 +++++++++++++++++++++++++++++++++++ 3 files changed, 365 insertions(+) create mode 100644 pkg/toolsets/kiali/traces.go diff --git a/pkg/kiali/mesh_test.go b/pkg/kiali/mesh_test.go index a729015d..89994c4e 100644 --- a/pkg/kiali/mesh_test.go +++ b/pkg/kiali/mesh_test.go @@ -38,3 +38,29 @@ func (s *KialiSuite) TestMeshStatus() { }) } + +func (s *KialiSuite) TestTraceDetails() { + var capturedURL *url.URL + s.MockServer.Config().BearerToken = "token-xyz" + s.MockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + u := *r.URL + capturedURL = &u + _, _ = w.Write([]byte(`{"traceId":"test-trace-123","spans":[]}`)) + })) + + s.Config = test.Must(config.ReadToml([]byte(fmt.Sprintf(` + [toolset_configs.kiali] + url = "%s" + `, s.MockServer.Config().Host)))) + k := NewKiali(s.Config, s.MockServer.Config()) + + traceId := "test-trace-123" + out, err := k.TraceDetails(s.T().Context(), traceId) + s.Require().NoError(err, "Expected no error executing request") + s.Run("response body is correct", func() { + s.Contains(out, traceId, "Response should contain trace ID") + }) + s.Run("path is correct", func() { + s.Equal("/api/traces/test-trace-123", capturedURL.Path, "Unexpected path") + }) +} diff --git a/pkg/kiali/traces.go b/pkg/kiali/traces.go index ba54b54a..465f4f46 100644 --- a/pkg/kiali/traces.go +++ b/pkg/kiali/traces.go @@ -105,3 +105,16 @@ func (k *Kiali) WorkloadTraces(ctx context.Context, namespace string, workload s return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil) } + +// TraceDetails returns detailed information for a specific trace by its ID. +// Parameters: +// - traceId: the unique identifier of the trace +func (k *Kiali) TraceDetails(ctx context.Context, traceId string) (string, error) { + if traceId == "" { + return "", fmt.Errorf("trace ID is required") + } + + endpoint := fmt.Sprintf("/api/traces/%s", url.PathEscape(traceId)) + + return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil) +} diff --git a/pkg/toolsets/kiali/traces.go b/pkg/toolsets/kiali/traces.go new file mode 100644 index 00000000..3be787b1 --- /dev/null +++ b/pkg/toolsets/kiali/traces.go @@ -0,0 +1,326 @@ +package kiali + +import ( + "fmt" + + "github.com/google/jsonschema-go/jsonschema" + "k8s.io/utils/ptr" + + "github.com/containers/kubernetes-mcp-server/pkg/api" +) + +func initTraces() []api.ServerTool { + ret := make([]api.ServerTool, 0) + + // App traces tool + ret = append(ret, api.ServerTool{ + Tool: api.Tool{ + Name: "app_traces", + Description: "Get distributed tracing data for a specific app in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Description: "Namespace containing the app", + }, + "app": { + Type: "string", + Description: "Name of the app to get traces for", + }, + "startMicros": { + Type: "string", + Description: "Start time for traces in microseconds since epoch (optional)", + }, + "endMicros": { + Type: "string", + Description: "End time for traces in microseconds since epoch (optional)", + }, + "limit": { + Type: "integer", + Description: "Maximum number of traces to return (default: 100)", + Minimum: ptr.To(float64(1)), + }, + "minDuration": { + Type: "integer", + Description: "Minimum trace duration in microseconds (optional)", + Minimum: ptr.To(float64(0)), + }, + "tags": { + Type: "string", + Description: "JSON string of tags to filter traces (optional)", + }, + "clusterName": { + Type: "string", + Description: "Cluster name for multi-cluster environments (optional)", + }, + }, + Required: []string{"namespace", "app"}, + }, + Annotations: api.ToolAnnotations{ + Title: "App: Traces", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, + Handler: appTracesHandler, + }) + + // Service traces tool + ret = append(ret, api.ServerTool{ + Tool: api.Tool{ + Name: "service_traces", + Description: "Get distributed tracing data for a specific service in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Description: "Namespace containing the service", + }, + "service": { + Type: "string", + Description: "Name of the service to get traces for", + }, + "startMicros": { + Type: "string", + Description: "Start time for traces in microseconds since epoch (optional)", + }, + "endMicros": { + Type: "string", + Description: "End time for traces in microseconds since epoch (optional)", + }, + "limit": { + Type: "integer", + Description: "Maximum number of traces to return (default: 100)", + Minimum: ptr.To(float64(1)), + }, + "minDuration": { + Type: "integer", + Description: "Minimum trace duration in microseconds (optional)", + Minimum: ptr.To(float64(0)), + }, + "tags": { + Type: "string", + Description: "JSON string of tags to filter traces (optional)", + }, + "clusterName": { + Type: "string", + Description: "Cluster name for multi-cluster environments (optional)", + }, + }, + Required: []string{"namespace", "service"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Service: Traces", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, + Handler: serviceTracesHandler, + }) + + // Workload traces tool + ret = append(ret, api.ServerTool{ + Tool: api.Tool{ + Name: "workload_traces", + Description: "Get distributed tracing data for a specific workload in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis. Note: startMicros and endMicros are typically required by the Kiali API.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Description: "Namespace containing the workload", + }, + "workload": { + Type: "string", + Description: "Name of the workload to get traces for", + }, + "startMicros": { + Type: "string", + Description: "Start time for traces in microseconds since epoch (required by Kiali API)", + }, + "endMicros": { + Type: "string", + Description: "End time for traces in microseconds since epoch (required by Kiali API)", + }, + "limit": { + Type: "integer", + Description: "Maximum number of traces to return (default: 100)", + Minimum: ptr.To(float64(1)), + }, + "minDuration": { + Type: "integer", + Description: "Minimum trace duration in microseconds (optional)", + Minimum: ptr.To(float64(0)), + }, + "tags": { + Type: "string", + Description: "JSON string of tags to filter traces (optional)", + }, + "clusterName": { + Type: "string", + Description: "Cluster name for multi-cluster environments (optional)", + }, + }, + Required: []string{"namespace", "workload"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Workload: Traces", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, + Handler: workloadTracesHandler, + }) + + // Trace details tool + ret = append(ret, api.ServerTool{ + Tool: api.Tool{ + Name: "trace_details", + Description: "Get detailed information for a specific trace by its ID. Returns complete trace information including all spans, timing details, and metadata for in-depth analysis.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "traceId": { + Type: "string", + Description: "Unique identifier of the trace to retrieve", + }, + }, + Required: []string{"traceId"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Trace: Details", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, + Handler: traceDetailsHandler, + }) + + return ret +} + +func appTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + // Extract parameters + namespace := params.GetArguments()["namespace"].(string) + app := params.GetArguments()["app"].(string) + + // Build query parameters from optional arguments + queryParams := make(map[string]string) + if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { + queryParams["startMicros"] = startMicros + } + if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { + queryParams["endMicros"] = endMicros + } + if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { + queryParams["limit"] = limit + } + if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { + queryParams["minDuration"] = minDuration + } + if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { + queryParams["tags"] = tags + } + if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { + queryParams["clusterName"] = clusterName + } + k := params.NewKiali() + content, err := k.AppTraces(params.Context, namespace, app, queryParams) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get app traces: %v", err)), nil + } + return api.NewToolCallResult(content, nil), nil +} + +func serviceTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + // Extract parameters + namespace := params.GetArguments()["namespace"].(string) + service := params.GetArguments()["service"].(string) + + // Build query parameters from optional arguments + queryParams := make(map[string]string) + if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { + queryParams["startMicros"] = startMicros + } + if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { + queryParams["endMicros"] = endMicros + } + if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { + queryParams["limit"] = limit + } + if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { + queryParams["minDuration"] = minDuration + } + if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { + queryParams["tags"] = tags + } + if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { + queryParams["clusterName"] = clusterName + } + + k := params.NewKiali() + content, err := k.ServiceTraces(params.Context, namespace, service, queryParams) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get service traces: %v", err)), nil + } + return api.NewToolCallResult(content, nil), nil +} + +func workloadTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + // Extract parameters + namespace := params.GetArguments()["namespace"].(string) + workload := params.GetArguments()["workload"].(string) + + // Build query parameters from optional arguments + queryParams := make(map[string]string) + if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { + queryParams["startMicros"] = startMicros + } + if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { + queryParams["endMicros"] = endMicros + } + if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { + queryParams["limit"] = limit + } + if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { + queryParams["minDuration"] = minDuration + } + if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { + queryParams["tags"] = tags + } + if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { + queryParams["clusterName"] = clusterName + } + + k := params.NewKiali() + content, err := k.WorkloadTraces(params.Context, namespace, workload, queryParams) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get workload traces: %v", err)), nil + } + return api.NewToolCallResult(content, nil), nil +} + +func traceDetailsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + // Extract required parameter + traceId, ok := params.GetArguments()["traceId"].(string) + if !ok || traceId == "" { + return api.NewToolCallResult("", fmt.Errorf("traceId parameter is required")), nil + } + + k := params.NewKiali() + content, err := k.TraceDetails(params.Context, traceId) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get trace details: %v", err)), nil + } + return api.NewToolCallResult(content, nil), nil +} From 37b8503e318fb11709a56627b3c9ebffe5f249db Mon Sep 17 00:00:00 2001 From: josunect Date: Wed, 19 Nov 2025 14:34:30 +0000 Subject: [PATCH 2/4] Fix error when startMicros not provided Signed-off-by: josunect --- README.md | 2 +- pkg/toolsets/kiali/traces.go | 184 +++++++++++++++++++++++++++++------ 2 files changed, 154 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 0da7a9dd..419eef62 100644 --- a/README.md +++ b/README.md @@ -390,7 +390,7 @@ In case multi-cluster support is enabled (default) and you have access to multip - **kiali_get_traces** - Gets traces for a specific resource (app, service, workload) in a namespace - `clusterName` (`string`) - Cluster name for multi-cluster environments (optional) - - `endMicros` (`string`) - End time for traces in microseconds since epoch (optional) + - `endMicros` (`string`) - End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided) - `limit` (`integer`) - Maximum number of traces to return (default: 100) - `minDuration` (`integer`) - Minimum trace duration in microseconds (optional) - `namespace` (`string`) **(required)** - Namespace to get resources from diff --git a/pkg/toolsets/kiali/traces.go b/pkg/toolsets/kiali/traces.go index 3be787b1..074acdfc 100644 --- a/pkg/toolsets/kiali/traces.go +++ b/pkg/toolsets/kiali/traces.go @@ -2,6 +2,8 @@ package kiali import ( "fmt" + "strconv" + "time" "github.com/google/jsonschema-go/jsonschema" "k8s.io/utils/ptr" @@ -30,11 +32,11 @@ func initTraces() []api.ServerTool { }, "startMicros": { Type: "string", - Description: "Start time for traces in microseconds since epoch (optional)", + Description: "Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided)", }, "endMicros": { Type: "string", - Description: "End time for traces in microseconds since epoch (optional)", + Description: "End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided)", }, "limit": { Type: "integer", @@ -86,11 +88,11 @@ func initTraces() []api.ServerTool { }, "startMicros": { Type: "string", - Description: "Start time for traces in microseconds since epoch (optional)", + Description: "Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided)", }, "endMicros": { Type: "string", - Description: "End time for traces in microseconds since epoch (optional)", + Description: "End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided)", }, "limit": { Type: "integer", @@ -128,7 +130,7 @@ func initTraces() []api.ServerTool { ret = append(ret, api.ServerTool{ Tool: api.Tool{ Name: "workload_traces", - Description: "Get distributed tracing data for a specific workload in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis. Note: startMicros and endMicros are typically required by the Kiali API.", + Description: "Get distributed tracing data for a specific workload in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis.", InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -142,11 +144,11 @@ func initTraces() []api.ServerTool { }, "startMicros": { Type: "string", - Description: "Start time for traces in microseconds since epoch (required by Kiali API)", + Description: "Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided)", }, "endMicros": { Type: "string", - Description: "End time for traces in microseconds since epoch (required by Kiali API)", + Description: "End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided)", }, "limit": { Type: "integer", @@ -216,17 +218,57 @@ func appTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) // Build query parameters from optional arguments queryParams := make(map[string]string) - if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { - queryParams["startMicros"] = startMicros + + // Handle startMicros: if not provided, default to 10 minutes ago + var startMicros string + if startMicrosVal, ok := params.GetArguments()["startMicros"].(string); ok && startMicrosVal != "" { + startMicros = startMicrosVal + } else { + // Default to 10 minutes before current time + now := time.Now() + tenMinutesAgo := now.Add(-10 * time.Minute) + startMicros = strconv.FormatInt(tenMinutesAgo.UnixMicro(), 10) } - if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { - queryParams["endMicros"] = endMicros + queryParams["startMicros"] = startMicros + + // Handle endMicros: if not provided, default to 10 minutes after startMicros + var endMicros string + if endMicrosVal, ok := params.GetArguments()["endMicros"].(string); ok && endMicrosVal != "" { + endMicros = endMicrosVal + } else { + // Parse startMicros to calculate endMicros + startMicrosInt, err := strconv.ParseInt(startMicros, 10, 64) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("invalid startMicros value: %v", err)), nil + } + startTime := time.UnixMicro(startMicrosInt) + endTime := startTime.Add(10 * time.Minute) + endMicros = strconv.FormatInt(endTime.UnixMicro(), 10) } - if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { - queryParams["limit"] = limit + queryParams["endMicros"] = endMicros + + // Handle limit: convert integer to string if provided + if limit := params.GetArguments()["limit"]; limit != nil { + switch v := limit.(type) { + case float64: + queryParams["limit"] = fmt.Sprintf("%.0f", v) + case int: + queryParams["limit"] = fmt.Sprintf("%d", v) + case int64: + queryParams["limit"] = fmt.Sprintf("%d", v) + } } - if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { - queryParams["minDuration"] = minDuration + + // Handle minDuration: convert integer to string if provided + if minDuration := params.GetArguments()["minDuration"]; minDuration != nil { + switch v := minDuration.(type) { + case float64: + queryParams["minDuration"] = fmt.Sprintf("%.0f", v) + case int: + queryParams["minDuration"] = fmt.Sprintf("%d", v) + case int64: + queryParams["minDuration"] = fmt.Sprintf("%d", v) + } } if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { queryParams["tags"] = tags @@ -249,17 +291,57 @@ func serviceTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, er // Build query parameters from optional arguments queryParams := make(map[string]string) - if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { - queryParams["startMicros"] = startMicros + + // Handle startMicros: if not provided, default to 10 minutes ago + var startMicros string + if startMicrosVal, ok := params.GetArguments()["startMicros"].(string); ok && startMicrosVal != "" { + startMicros = startMicrosVal + } else { + // Default to 10 minutes before current time + now := time.Now() + tenMinutesAgo := now.Add(-10 * time.Minute) + startMicros = strconv.FormatInt(tenMinutesAgo.UnixMicro(), 10) } - if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { - queryParams["endMicros"] = endMicros + queryParams["startMicros"] = startMicros + + // Handle endMicros: if not provided, default to 10 minutes after startMicros + var endMicros string + if endMicrosVal, ok := params.GetArguments()["endMicros"].(string); ok && endMicrosVal != "" { + endMicros = endMicrosVal + } else { + // Parse startMicros to calculate endMicros + startMicrosInt, err := strconv.ParseInt(startMicros, 10, 64) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("invalid startMicros value: %v", err)), nil + } + startTime := time.UnixMicro(startMicrosInt) + endTime := startTime.Add(10 * time.Minute) + endMicros = strconv.FormatInt(endTime.UnixMicro(), 10) } - if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { - queryParams["limit"] = limit + queryParams["endMicros"] = endMicros + + // Handle limit: convert integer to string if provided + if limit := params.GetArguments()["limit"]; limit != nil { + switch v := limit.(type) { + case float64: + queryParams["limit"] = fmt.Sprintf("%.0f", v) + case int: + queryParams["limit"] = fmt.Sprintf("%d", v) + case int64: + queryParams["limit"] = fmt.Sprintf("%d", v) + } } - if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { - queryParams["minDuration"] = minDuration + + // Handle minDuration: convert integer to string if provided + if minDuration := params.GetArguments()["minDuration"]; minDuration != nil { + switch v := minDuration.(type) { + case float64: + queryParams["minDuration"] = fmt.Sprintf("%.0f", v) + case int: + queryParams["minDuration"] = fmt.Sprintf("%d", v) + case int64: + queryParams["minDuration"] = fmt.Sprintf("%d", v) + } } if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { queryParams["tags"] = tags @@ -283,17 +365,57 @@ func workloadTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, e // Build query parameters from optional arguments queryParams := make(map[string]string) - if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { - queryParams["startMicros"] = startMicros + + // Handle startMicros: if not provided, default to 10 minutes ago + var startMicros string + if startMicrosVal, ok := params.GetArguments()["startMicros"].(string); ok && startMicrosVal != "" { + startMicros = startMicrosVal + } else { + // Default to 10 minutes before current time + now := time.Now() + tenMinutesAgo := now.Add(-10 * time.Minute) + startMicros = strconv.FormatInt(tenMinutesAgo.UnixMicro(), 10) } - if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { - queryParams["endMicros"] = endMicros + queryParams["startMicros"] = startMicros + + // Handle endMicros: if not provided, default to 10 minutes after startMicros + var endMicros string + if endMicrosVal, ok := params.GetArguments()["endMicros"].(string); ok && endMicrosVal != "" { + endMicros = endMicrosVal + } else { + // Parse startMicros to calculate endMicros + startMicrosInt, err := strconv.ParseInt(startMicros, 10, 64) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("invalid startMicros value: %v", err)), nil + } + startTime := time.UnixMicro(startMicrosInt) + endTime := startTime.Add(10 * time.Minute) + endMicros = strconv.FormatInt(endTime.UnixMicro(), 10) } - if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { - queryParams["limit"] = limit + queryParams["endMicros"] = endMicros + + // Handle limit: convert integer to string if provided + if limit := params.GetArguments()["limit"]; limit != nil { + switch v := limit.(type) { + case float64: + queryParams["limit"] = fmt.Sprintf("%.0f", v) + case int: + queryParams["limit"] = fmt.Sprintf("%d", v) + case int64: + queryParams["limit"] = fmt.Sprintf("%d", v) + } } - if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { - queryParams["minDuration"] = minDuration + + // Handle minDuration: convert integer to string if provided + if minDuration := params.GetArguments()["minDuration"]; minDuration != nil { + switch v := minDuration.(type) { + case float64: + queryParams["minDuration"] = fmt.Sprintf("%.0f", v) + case int: + queryParams["minDuration"] = fmt.Sprintf("%d", v) + case int64: + queryParams["minDuration"] = fmt.Sprintf("%d", v) + } } if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { queryParams["tags"] = tags From f15345e22e1c801033889529073f8f37239712af Mon Sep 17 00:00:00 2001 From: josunect Date: Tue, 25 Nov 2025 16:50:35 +0000 Subject: [PATCH 3/4] Consolidate trace id in get_traces Signed-off-by: josunect --- README.md | 21 +- pkg/toolsets/kiali/get_traces.go | 114 ++++++-- pkg/toolsets/kiali/traces.go | 448 ------------------------------- 3 files changed, 99 insertions(+), 484 deletions(-) delete mode 100644 pkg/toolsets/kiali/traces.go diff --git a/README.md b/README.md index 419eef62..13a57d9e 100644 --- a/README.md +++ b/README.md @@ -388,16 +388,17 @@ In case multi-cluster support is enabled (default) and you have access to multip - `tail` (`integer`) - Number of lines to retrieve from the end of logs (default: 100) - `workload` (`string`) **(required)** - Name of the workload to get logs for -- **kiali_get_traces** - Gets traces for a specific resource (app, service, workload) in a namespace - - `clusterName` (`string`) - Cluster name for multi-cluster environments (optional) - - `endMicros` (`string`) - End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided) - - `limit` (`integer`) - Maximum number of traces to return (default: 100) - - `minDuration` (`integer`) - Minimum trace duration in microseconds (optional) - - `namespace` (`string`) **(required)** - Namespace to get resources from - - `resource_name` (`string`) **(required)** - Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all). - - `resource_type` (`string`) **(required)** - Type of resource to get metrics for (app, service, workload) - - `startMicros` (`string`) - Start time for traces in microseconds since epoch (optional) - - `tags` (`string`) - JSON string of tags to filter traces (optional) +- **kiali_get_traces** - Gets traces for a specific resource (app, service, workload) in a namespace, or gets detailed information for a specific trace by its ID. If traceId is provided, it returns detailed trace information and other parameters are not required. + - `traceId` (`string`) - Unique identifier of the trace to retrieve detailed information for. If provided, this will return detailed trace information and other parameters (resource_type, namespace, resource_name) are not required. + - `resource_type` (`string`) - Type of resource to get traces for (app, service, workload). Required if traceId is not provided. + - `namespace` (`string`) - Namespace to get resources from. Required if traceId is not provided. + - `resource_name` (`string`) - Name of the resource to get traces for. Required if traceId is not provided. + - `startMicros` (`string`) - Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided, only used when traceId is not provided) + - `endMicros` (`string`) - End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided, only used when traceId is not provided) + - `limit` (`integer`) - Maximum number of traces to return (default: 100, only used when traceId is not provided) + - `minDuration` (`integer`) - Minimum trace duration in microseconds (optional, only used when traceId is not provided) + - `tags` (`string`) - JSON string of tags to filter traces (optional, only used when traceId is not provided) + - `clusterName` (`string`) - Cluster name for multi-cluster environments (optional, only used when traceId is not provided) diff --git a/pkg/toolsets/kiali/get_traces.go b/pkg/toolsets/kiali/get_traces.go index 65db5647..4d3080bb 100644 --- a/pkg/toolsets/kiali/get_traces.go +++ b/pkg/toolsets/kiali/get_traces.go @@ -3,7 +3,9 @@ package kiali import ( "context" "fmt" + "strconv" "strings" + "time" "github.com/google/jsonschema-go/jsonschema" "k8s.io/utils/ptr" @@ -44,54 +46,58 @@ func initGetTraces() []api.ServerTool { ret = append(ret, api.ServerTool{ Tool: api.Tool{ Name: "kiali_get_traces", - Description: "Gets traces for a specific resource (app, service, workload) in a namespace", + Description: "Gets traces for a specific resource (app, service, workload) in a namespace, or gets detailed information for a specific trace by its ID. If traceId is provided, it returns detailed trace information and other parameters are not required.", InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ + "traceId": { + Type: "string", + Description: "Unique identifier of the trace to retrieve detailed information for. If provided, this will return detailed trace information and other parameters (resource_type, namespace, resource_name) are not required.", + }, "resource_type": { Type: "string", - Description: "Type of resource to get metrics for (app, service, workload)", + Description: "Type of resource to get traces for (app, service, workload). Required if traceId is not provided.", Enum: []any{"app", "service", "workload"}, }, "namespace": { Type: "string", - Description: "Namespace to get resources from", + Description: "Namespace to get resources from. Required if traceId is not provided.", }, "resource_name": { Type: "string", - Description: "Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all).", + Description: "Name of the resource to get traces for. Required if traceId is not provided.", }, "startMicros": { Type: "string", - Description: "Start time for traces in microseconds since epoch (optional)", + Description: "Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided, only used when traceId is not provided)", }, "endMicros": { Type: "string", - Description: "End time for traces in microseconds since epoch (optional)", + Description: "End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided, only used when traceId is not provided)", }, "limit": { Type: "integer", - Description: "Maximum number of traces to return (default: 100)", + Description: "Maximum number of traces to return (default: 100, only used when traceId is not provided)", Minimum: ptr.To(float64(1)), }, "minDuration": { Type: "integer", - Description: "Minimum trace duration in microseconds (optional)", + Description: "Minimum trace duration in microseconds (optional, only used when traceId is not provided)", Minimum: ptr.To(float64(0)), }, "tags": { Type: "string", - Description: "JSON string of tags to filter traces (optional)", + Description: "JSON string of tags to filter traces (optional, only used when traceId is not provided)", }, "clusterName": { Type: "string", - Description: "Cluster name for multi-cluster environments (optional)", + Description: "Cluster name for multi-cluster environments (optional, only used when traceId is not provided)", }, }, - Required: []string{"resource_type", "namespace", "resource_name"}, + Required: []string{}, }, Annotations: api.ToolAnnotations{ - Title: "Get Traces for a Resource", + Title: "Get Traces for a Resource or Trace Details", ReadOnlyHint: ptr.To(true), DestructiveHint: ptr.To(false), IdempotentHint: ptr.To(true), @@ -104,7 +110,19 @@ func initGetTraces() []api.ServerTool { } func TracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters + k := params.NewKiali() + + // Check if traceId is provided - if so, get trace details directly + if traceIdVal, ok := params.GetArguments()["traceId"].(string); ok && traceIdVal != "" { + traceId := strings.TrimSpace(traceIdVal) + content, err := k.TraceDetails(params.Context, traceId) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get trace details: %v", err)), nil + } + return api.NewToolCallResult(content, nil), nil + } + + // Otherwise, get traces for a resource (existing functionality) resourceType, _ := params.GetArguments()["resource_type"].(string) namespace, _ := params.GetArguments()["namespace"].(string) resourceName, _ := params.GetArguments()["resource_name"].(string) @@ -114,34 +132,78 @@ func TracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { resourceName = strings.TrimSpace(resourceName) if resourceType == "" { - return api.NewToolCallResult("", fmt.Errorf("resource_type is required")), nil + return api.NewToolCallResult("", fmt.Errorf("resource_type is required when traceId is not provided")), nil } if namespace == "" || len(strings.Split(namespace, ",")) != 1 { - return api.NewToolCallResult("", fmt.Errorf("namespace is required")), nil + return api.NewToolCallResult("", fmt.Errorf("namespace is required when traceId is not provided")), nil } if resourceName == "" { - return api.NewToolCallResult("", fmt.Errorf("resource_name is required")), nil + return api.NewToolCallResult("", fmt.Errorf("resource_name is required when traceId is not provided")), nil } - k := params.NewKiali() - ops, ok := tracesOpsMap[resourceType] if !ok { return api.NewToolCallResult("", fmt.Errorf("invalid resource type: %s", resourceType)), nil } queryParams := make(map[string]string) - if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { - queryParams["startMicros"] = startMicros + + // Handle startMicros: if not provided, default to 10 minutes ago + var startMicros string + if startMicrosVal, ok := params.GetArguments()["startMicros"].(string); ok && startMicrosVal != "" { + startMicros = startMicrosVal + } else { + // Default to 10 minutes before current time + now := time.Now() + tenMinutesAgo := now.Add(-10 * time.Minute) + startMicros = strconv.FormatInt(tenMinutesAgo.UnixMicro(), 10) } - if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { - queryParams["endMicros"] = endMicros + queryParams["startMicros"] = startMicros + + // Handle endMicros: if not provided, default to 10 minutes after startMicros + var endMicros string + if endMicrosVal, ok := params.GetArguments()["endMicros"].(string); ok && endMicrosVal != "" { + endMicros = endMicrosVal + } else { + // Parse startMicros to calculate endMicros + startMicrosInt, err := strconv.ParseInt(startMicros, 10, 64) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("invalid startMicros value: %v", err)), nil + } + startTime := time.UnixMicro(startMicrosInt) + endTime := startTime.Add(10 * time.Minute) + endMicros = strconv.FormatInt(endTime.UnixMicro(), 10) } - if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { - queryParams["limit"] = limit + queryParams["endMicros"] = endMicros + // Handle limit: convert integer to string if provided + if limit := params.GetArguments()["limit"]; limit != nil { + switch v := limit.(type) { + case float64: + queryParams["limit"] = fmt.Sprintf("%.0f", v) + case int: + queryParams["limit"] = fmt.Sprintf("%d", v) + case int64: + queryParams["limit"] = fmt.Sprintf("%d", v) + case string: + if v != "" { + queryParams["limit"] = v + } + } } - if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { - queryParams["minDuration"] = minDuration + // Handle minDuration: convert integer to string if provided + if minDuration := params.GetArguments()["minDuration"]; minDuration != nil { + switch v := minDuration.(type) { + case float64: + queryParams["minDuration"] = fmt.Sprintf("%.0f", v) + case int: + queryParams["minDuration"] = fmt.Sprintf("%d", v) + case int64: + queryParams["minDuration"] = fmt.Sprintf("%d", v) + case string: + if v != "" { + queryParams["minDuration"] = v + } + } } if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { queryParams["tags"] = tags diff --git a/pkg/toolsets/kiali/traces.go b/pkg/toolsets/kiali/traces.go deleted file mode 100644 index 074acdfc..00000000 --- a/pkg/toolsets/kiali/traces.go +++ /dev/null @@ -1,448 +0,0 @@ -package kiali - -import ( - "fmt" - "strconv" - "time" - - "github.com/google/jsonschema-go/jsonschema" - "k8s.io/utils/ptr" - - "github.com/containers/kubernetes-mcp-server/pkg/api" -) - -func initTraces() []api.ServerTool { - ret := make([]api.ServerTool, 0) - - // App traces tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "app_traces", - Description: "Get distributed tracing data for a specific app in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the app", - }, - "app": { - Type: "string", - Description: "Name of the app to get traces for", - }, - "startMicros": { - Type: "string", - Description: "Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided)", - }, - "endMicros": { - Type: "string", - Description: "End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided)", - }, - "limit": { - Type: "integer", - Description: "Maximum number of traces to return (default: 100)", - Minimum: ptr.To(float64(1)), - }, - "minDuration": { - Type: "integer", - Description: "Minimum trace duration in microseconds (optional)", - Minimum: ptr.To(float64(0)), - }, - "tags": { - Type: "string", - Description: "JSON string of tags to filter traces (optional)", - }, - "clusterName": { - Type: "string", - Description: "Cluster name for multi-cluster environments (optional)", - }, - }, - Required: []string{"namespace", "app"}, - }, - Annotations: api.ToolAnnotations{ - Title: "App: Traces", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, - Handler: appTracesHandler, - }) - - // Service traces tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "service_traces", - Description: "Get distributed tracing data for a specific service in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the service", - }, - "service": { - Type: "string", - Description: "Name of the service to get traces for", - }, - "startMicros": { - Type: "string", - Description: "Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided)", - }, - "endMicros": { - Type: "string", - Description: "End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided)", - }, - "limit": { - Type: "integer", - Description: "Maximum number of traces to return (default: 100)", - Minimum: ptr.To(float64(1)), - }, - "minDuration": { - Type: "integer", - Description: "Minimum trace duration in microseconds (optional)", - Minimum: ptr.To(float64(0)), - }, - "tags": { - Type: "string", - Description: "JSON string of tags to filter traces (optional)", - }, - "clusterName": { - Type: "string", - Description: "Cluster name for multi-cluster environments (optional)", - }, - }, - Required: []string{"namespace", "service"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Service: Traces", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, - Handler: serviceTracesHandler, - }) - - // Workload traces tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "workload_traces", - Description: "Get distributed tracing data for a specific workload in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the workload", - }, - "workload": { - Type: "string", - Description: "Name of the workload to get traces for", - }, - "startMicros": { - Type: "string", - Description: "Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided)", - }, - "endMicros": { - Type: "string", - Description: "End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided)", - }, - "limit": { - Type: "integer", - Description: "Maximum number of traces to return (default: 100)", - Minimum: ptr.To(float64(1)), - }, - "minDuration": { - Type: "integer", - Description: "Minimum trace duration in microseconds (optional)", - Minimum: ptr.To(float64(0)), - }, - "tags": { - Type: "string", - Description: "JSON string of tags to filter traces (optional)", - }, - "clusterName": { - Type: "string", - Description: "Cluster name for multi-cluster environments (optional)", - }, - }, - Required: []string{"namespace", "workload"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Workload: Traces", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, - Handler: workloadTracesHandler, - }) - - // Trace details tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "trace_details", - Description: "Get detailed information for a specific trace by its ID. Returns complete trace information including all spans, timing details, and metadata for in-depth analysis.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "traceId": { - Type: "string", - Description: "Unique identifier of the trace to retrieve", - }, - }, - Required: []string{"traceId"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Trace: Details", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, - Handler: traceDetailsHandler, - }) - - return ret -} - -func appTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespace := params.GetArguments()["namespace"].(string) - app := params.GetArguments()["app"].(string) - - // Build query parameters from optional arguments - queryParams := make(map[string]string) - - // Handle startMicros: if not provided, default to 10 minutes ago - var startMicros string - if startMicrosVal, ok := params.GetArguments()["startMicros"].(string); ok && startMicrosVal != "" { - startMicros = startMicrosVal - } else { - // Default to 10 minutes before current time - now := time.Now() - tenMinutesAgo := now.Add(-10 * time.Minute) - startMicros = strconv.FormatInt(tenMinutesAgo.UnixMicro(), 10) - } - queryParams["startMicros"] = startMicros - - // Handle endMicros: if not provided, default to 10 minutes after startMicros - var endMicros string - if endMicrosVal, ok := params.GetArguments()["endMicros"].(string); ok && endMicrosVal != "" { - endMicros = endMicrosVal - } else { - // Parse startMicros to calculate endMicros - startMicrosInt, err := strconv.ParseInt(startMicros, 10, 64) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("invalid startMicros value: %v", err)), nil - } - startTime := time.UnixMicro(startMicrosInt) - endTime := startTime.Add(10 * time.Minute) - endMicros = strconv.FormatInt(endTime.UnixMicro(), 10) - } - queryParams["endMicros"] = endMicros - - // Handle limit: convert integer to string if provided - if limit := params.GetArguments()["limit"]; limit != nil { - switch v := limit.(type) { - case float64: - queryParams["limit"] = fmt.Sprintf("%.0f", v) - case int: - queryParams["limit"] = fmt.Sprintf("%d", v) - case int64: - queryParams["limit"] = fmt.Sprintf("%d", v) - } - } - - // Handle minDuration: convert integer to string if provided - if minDuration := params.GetArguments()["minDuration"]; minDuration != nil { - switch v := minDuration.(type) { - case float64: - queryParams["minDuration"] = fmt.Sprintf("%.0f", v) - case int: - queryParams["minDuration"] = fmt.Sprintf("%d", v) - case int64: - queryParams["minDuration"] = fmt.Sprintf("%d", v) - } - } - if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { - queryParams["tags"] = tags - } - if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { - queryParams["clusterName"] = clusterName - } - k := params.NewKiali() - content, err := k.AppTraces(params.Context, namespace, app, queryParams) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get app traces: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func serviceTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespace := params.GetArguments()["namespace"].(string) - service := params.GetArguments()["service"].(string) - - // Build query parameters from optional arguments - queryParams := make(map[string]string) - - // Handle startMicros: if not provided, default to 10 minutes ago - var startMicros string - if startMicrosVal, ok := params.GetArguments()["startMicros"].(string); ok && startMicrosVal != "" { - startMicros = startMicrosVal - } else { - // Default to 10 minutes before current time - now := time.Now() - tenMinutesAgo := now.Add(-10 * time.Minute) - startMicros = strconv.FormatInt(tenMinutesAgo.UnixMicro(), 10) - } - queryParams["startMicros"] = startMicros - - // Handle endMicros: if not provided, default to 10 minutes after startMicros - var endMicros string - if endMicrosVal, ok := params.GetArguments()["endMicros"].(string); ok && endMicrosVal != "" { - endMicros = endMicrosVal - } else { - // Parse startMicros to calculate endMicros - startMicrosInt, err := strconv.ParseInt(startMicros, 10, 64) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("invalid startMicros value: %v", err)), nil - } - startTime := time.UnixMicro(startMicrosInt) - endTime := startTime.Add(10 * time.Minute) - endMicros = strconv.FormatInt(endTime.UnixMicro(), 10) - } - queryParams["endMicros"] = endMicros - - // Handle limit: convert integer to string if provided - if limit := params.GetArguments()["limit"]; limit != nil { - switch v := limit.(type) { - case float64: - queryParams["limit"] = fmt.Sprintf("%.0f", v) - case int: - queryParams["limit"] = fmt.Sprintf("%d", v) - case int64: - queryParams["limit"] = fmt.Sprintf("%d", v) - } - } - - // Handle minDuration: convert integer to string if provided - if minDuration := params.GetArguments()["minDuration"]; minDuration != nil { - switch v := minDuration.(type) { - case float64: - queryParams["minDuration"] = fmt.Sprintf("%.0f", v) - case int: - queryParams["minDuration"] = fmt.Sprintf("%d", v) - case int64: - queryParams["minDuration"] = fmt.Sprintf("%d", v) - } - } - if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { - queryParams["tags"] = tags - } - if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { - queryParams["clusterName"] = clusterName - } - - k := params.NewKiali() - content, err := k.ServiceTraces(params.Context, namespace, service, queryParams) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get service traces: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func workloadTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespace := params.GetArguments()["namespace"].(string) - workload := params.GetArguments()["workload"].(string) - - // Build query parameters from optional arguments - queryParams := make(map[string]string) - - // Handle startMicros: if not provided, default to 10 minutes ago - var startMicros string - if startMicrosVal, ok := params.GetArguments()["startMicros"].(string); ok && startMicrosVal != "" { - startMicros = startMicrosVal - } else { - // Default to 10 minutes before current time - now := time.Now() - tenMinutesAgo := now.Add(-10 * time.Minute) - startMicros = strconv.FormatInt(tenMinutesAgo.UnixMicro(), 10) - } - queryParams["startMicros"] = startMicros - - // Handle endMicros: if not provided, default to 10 minutes after startMicros - var endMicros string - if endMicrosVal, ok := params.GetArguments()["endMicros"].(string); ok && endMicrosVal != "" { - endMicros = endMicrosVal - } else { - // Parse startMicros to calculate endMicros - startMicrosInt, err := strconv.ParseInt(startMicros, 10, 64) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("invalid startMicros value: %v", err)), nil - } - startTime := time.UnixMicro(startMicrosInt) - endTime := startTime.Add(10 * time.Minute) - endMicros = strconv.FormatInt(endTime.UnixMicro(), 10) - } - queryParams["endMicros"] = endMicros - - // Handle limit: convert integer to string if provided - if limit := params.GetArguments()["limit"]; limit != nil { - switch v := limit.(type) { - case float64: - queryParams["limit"] = fmt.Sprintf("%.0f", v) - case int: - queryParams["limit"] = fmt.Sprintf("%d", v) - case int64: - queryParams["limit"] = fmt.Sprintf("%d", v) - } - } - - // Handle minDuration: convert integer to string if provided - if minDuration := params.GetArguments()["minDuration"]; minDuration != nil { - switch v := minDuration.(type) { - case float64: - queryParams["minDuration"] = fmt.Sprintf("%.0f", v) - case int: - queryParams["minDuration"] = fmt.Sprintf("%d", v) - case int64: - queryParams["minDuration"] = fmt.Sprintf("%d", v) - } - } - if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { - queryParams["tags"] = tags - } - if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { - queryParams["clusterName"] = clusterName - } - - k := params.NewKiali() - content, err := k.WorkloadTraces(params.Context, namespace, workload, queryParams) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get workload traces: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func traceDetailsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract required parameter - traceId, ok := params.GetArguments()["traceId"].(string) - if !ok || traceId == "" { - return api.NewToolCallResult("", fmt.Errorf("traceId parameter is required")), nil - } - - k := params.NewKiali() - content, err := k.TraceDetails(params.Context, traceId) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get trace details: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} From 358f4afe848a9830c17d97ab9494db340efad075 Mon Sep 17 00:00:00 2001 From: josunect Date: Wed, 26 Nov 2025 07:21:54 +0000 Subject: [PATCH 4/4] Order Signed-off-by: josunect --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 13a57d9e..7cc1c6d6 100644 --- a/README.md +++ b/README.md @@ -389,16 +389,16 @@ In case multi-cluster support is enabled (default) and you have access to multip - `workload` (`string`) **(required)** - Name of the workload to get logs for - **kiali_get_traces** - Gets traces for a specific resource (app, service, workload) in a namespace, or gets detailed information for a specific trace by its ID. If traceId is provided, it returns detailed trace information and other parameters are not required. - - `traceId` (`string`) - Unique identifier of the trace to retrieve detailed information for. If provided, this will return detailed trace information and other parameters (resource_type, namespace, resource_name) are not required. - - `resource_type` (`string`) - Type of resource to get traces for (app, service, workload). Required if traceId is not provided. - - `namespace` (`string`) - Namespace to get resources from. Required if traceId is not provided. - - `resource_name` (`string`) - Name of the resource to get traces for. Required if traceId is not provided. - - `startMicros` (`string`) - Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided, only used when traceId is not provided) + - `clusterName` (`string`) - Cluster name for multi-cluster environments (optional, only used when traceId is not provided) - `endMicros` (`string`) - End time for traces in microseconds since epoch (optional, defaults to 10 minutes after startMicros if not provided, only used when traceId is not provided) - `limit` (`integer`) - Maximum number of traces to return (default: 100, only used when traceId is not provided) - `minDuration` (`integer`) - Minimum trace duration in microseconds (optional, only used when traceId is not provided) - - `tags` (`string`) - JSON string of tags to filter traces (optional, only used when traceId is not provided) - - `clusterName` (`string`) - Cluster name for multi-cluster environments (optional, only used when traceId is not provided) + - `namespace` (`string`) - Namespace to get resources from. Required if traceId is not provided. + - `resource_name` (`string`) - Name of the resource to get traces for. Required if traceId is not provided. + - `resource_type` (`string`) - Type of resource to get traces for (app, service, workload). Required if traceId is not provided. + - `startMicros` (`string`) - Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided, only used when traceId is not provided) + - `tags` (`string`) - JSON string of tags to filter traces (optional, only used when traceId is not provided) + - `traceId` (`string`) - Unique identifier of the trace to retrieve detailed information for. If provided, this will return detailed trace information and other parameters (resource_type, namespace, resource_name) are not required.