Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

</details>

Expand Down
26 changes: 26 additions & 0 deletions pkg/kiali/mesh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}
13 changes: 13 additions & 0 deletions pkg/kiali/traces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
114 changes: 88 additions & 26 deletions pkg/toolsets/kiali/get_traces.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package kiali
import (
"context"
"fmt"
"strconv"
"strings"
"time"

"github.com/google/jsonschema-go/jsonschema"
"k8s.io/utils/ptr"
Expand Down Expand Up @@ -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),
Expand All @@ -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)
Expand All @@ -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
Expand Down