diff --git a/README.md b/README.md index 0da7a9ddd..7cc1c6d68 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) - - `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. + - `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) + - `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. diff --git a/pkg/kiali/mesh_test.go b/pkg/kiali/mesh_test.go index a729015d7..89994c4e4 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 ba54b54af..465f4f46c 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/get_traces.go b/pkg/toolsets/kiali/get_traces.go index 65db56470..4d3080bb1 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