Skip to content

Commit f912c98

Browse files
authored
feat(kiali): add Kiali trace details (#478)
* Add Kiali trace details Signed-off-by: josunect <jcordoba@redhat.com> * Fix error when startMicros not provided Signed-off-by: josunect <jcordoba@redhat.com> * Consolidate trace id in get_traces Signed-off-by: josunect <jcordoba@redhat.com> * Order Signed-off-by: josunect <jcordoba@redhat.com> --------- Signed-off-by: josunect <jcordoba@redhat.com>
1 parent 1c01e66 commit f912c98

File tree

4 files changed

+138
-36
lines changed

4 files changed

+138
-36
lines changed

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -388,16 +388,17 @@ In case multi-cluster support is enabled (default) and you have access to multip
388388
- `tail` (`integer`) - Number of lines to retrieve from the end of logs (default: 100)
389389
- `workload` (`string`) **(required)** - Name of the workload to get logs for
390390

391-
- **kiali_get_traces** - Gets traces for a specific resource (app, service, workload) in a namespace
392-
- `clusterName` (`string`) - Cluster name for multi-cluster environments (optional)
393-
- `endMicros` (`string`) - End time for traces in microseconds since epoch (optional)
394-
- `limit` (`integer`) - Maximum number of traces to return (default: 100)
395-
- `minDuration` (`integer`) - Minimum trace duration in microseconds (optional)
396-
- `namespace` (`string`) **(required)** - Namespace to get resources from
397-
- `resource_name` (`string`) **(required)** - Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all).
398-
- `resource_type` (`string`) **(required)** - Type of resource to get metrics for (app, service, workload)
399-
- `startMicros` (`string`) - Start time for traces in microseconds since epoch (optional)
400-
- `tags` (`string`) - JSON string of tags to filter traces (optional)
391+
- **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.
392+
- `clusterName` (`string`) - Cluster name for multi-cluster environments (optional, only used when traceId is not provided)
393+
- `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)
394+
- `limit` (`integer`) - Maximum number of traces to return (default: 100, only used when traceId is not provided)
395+
- `minDuration` (`integer`) - Minimum trace duration in microseconds (optional, only used when traceId is not provided)
396+
- `namespace` (`string`) - Namespace to get resources from. Required if traceId is not provided.
397+
- `resource_name` (`string`) - Name of the resource to get traces for. Required if traceId is not provided.
398+
- `resource_type` (`string`) - Type of resource to get traces for (app, service, workload). Required if traceId is not provided.
399+
- `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)
400+
- `tags` (`string`) - JSON string of tags to filter traces (optional, only used when traceId is not provided)
401+
- `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.
401402

402403
</details>
403404

pkg/kiali/mesh_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,29 @@ func (s *KialiSuite) TestMeshStatus() {
3838
})
3939

4040
}
41+
42+
func (s *KialiSuite) TestTraceDetails() {
43+
var capturedURL *url.URL
44+
s.MockServer.Config().BearerToken = "token-xyz"
45+
s.MockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46+
u := *r.URL
47+
capturedURL = &u
48+
_, _ = w.Write([]byte(`{"traceId":"test-trace-123","spans":[]}`))
49+
}))
50+
51+
s.Config = test.Must(config.ReadToml([]byte(fmt.Sprintf(`
52+
[toolset_configs.kiali]
53+
url = "%s"
54+
`, s.MockServer.Config().Host))))
55+
k := NewKiali(s.Config, s.MockServer.Config())
56+
57+
traceId := "test-trace-123"
58+
out, err := k.TraceDetails(s.T().Context(), traceId)
59+
s.Require().NoError(err, "Expected no error executing request")
60+
s.Run("response body is correct", func() {
61+
s.Contains(out, traceId, "Response should contain trace ID")
62+
})
63+
s.Run("path is correct", func() {
64+
s.Equal("/api/traces/test-trace-123", capturedURL.Path, "Unexpected path")
65+
})
66+
}

pkg/kiali/traces.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,16 @@ func (k *Kiali) WorkloadTraces(ctx context.Context, namespace string, workload s
105105

106106
return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil)
107107
}
108+
109+
// TraceDetails returns detailed information for a specific trace by its ID.
110+
// Parameters:
111+
// - traceId: the unique identifier of the trace
112+
func (k *Kiali) TraceDetails(ctx context.Context, traceId string) (string, error) {
113+
if traceId == "" {
114+
return "", fmt.Errorf("trace ID is required")
115+
}
116+
117+
endpoint := fmt.Sprintf("/api/traces/%s", url.PathEscape(traceId))
118+
119+
return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil)
120+
}

pkg/toolsets/kiali/get_traces.go

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package kiali
33
import (
44
"context"
55
"fmt"
6+
"strconv"
67
"strings"
8+
"time"
79

810
"github.com/google/jsonschema-go/jsonschema"
911
"k8s.io/utils/ptr"
@@ -44,54 +46,58 @@ func initGetTraces() []api.ServerTool {
4446
ret = append(ret, api.ServerTool{
4547
Tool: api.Tool{
4648
Name: "kiali_get_traces",
47-
Description: "Gets traces for a specific resource (app, service, workload) in a namespace",
49+
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.",
4850
InputSchema: &jsonschema.Schema{
4951
Type: "object",
5052
Properties: map[string]*jsonschema.Schema{
53+
"traceId": {
54+
Type: "string",
55+
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.",
56+
},
5157
"resource_type": {
5258
Type: "string",
53-
Description: "Type of resource to get metrics for (app, service, workload)",
59+
Description: "Type of resource to get traces for (app, service, workload). Required if traceId is not provided.",
5460
Enum: []any{"app", "service", "workload"},
5561
},
5662
"namespace": {
5763
Type: "string",
58-
Description: "Namespace to get resources from",
64+
Description: "Namespace to get resources from. Required if traceId is not provided.",
5965
},
6066
"resource_name": {
6167
Type: "string",
62-
Description: "Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all).",
68+
Description: "Name of the resource to get traces for. Required if traceId is not provided.",
6369
},
6470
"startMicros": {
6571
Type: "string",
66-
Description: "Start time for traces in microseconds since epoch (optional)",
72+
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)",
6773
},
6874
"endMicros": {
6975
Type: "string",
70-
Description: "End time for traces in microseconds since epoch (optional)",
76+
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)",
7177
},
7278
"limit": {
7379
Type: "integer",
74-
Description: "Maximum number of traces to return (default: 100)",
80+
Description: "Maximum number of traces to return (default: 100, only used when traceId is not provided)",
7581
Minimum: ptr.To(float64(1)),
7682
},
7783
"minDuration": {
7884
Type: "integer",
79-
Description: "Minimum trace duration in microseconds (optional)",
85+
Description: "Minimum trace duration in microseconds (optional, only used when traceId is not provided)",
8086
Minimum: ptr.To(float64(0)),
8187
},
8288
"tags": {
8389
Type: "string",
84-
Description: "JSON string of tags to filter traces (optional)",
90+
Description: "JSON string of tags to filter traces (optional, only used when traceId is not provided)",
8591
},
8692
"clusterName": {
8793
Type: "string",
88-
Description: "Cluster name for multi-cluster environments (optional)",
94+
Description: "Cluster name for multi-cluster environments (optional, only used when traceId is not provided)",
8995
},
9096
},
91-
Required: []string{"resource_type", "namespace", "resource_name"},
97+
Required: []string{},
9298
},
9399
Annotations: api.ToolAnnotations{
94-
Title: "Get Traces for a Resource",
100+
Title: "Get Traces for a Resource or Trace Details",
95101
ReadOnlyHint: ptr.To(true),
96102
DestructiveHint: ptr.To(false),
97103
IdempotentHint: ptr.To(true),
@@ -104,7 +110,19 @@ func initGetTraces() []api.ServerTool {
104110
}
105111

106112
func TracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
107-
// Extract parameters
113+
k := params.NewKiali()
114+
115+
// Check if traceId is provided - if so, get trace details directly
116+
if traceIdVal, ok := params.GetArguments()["traceId"].(string); ok && traceIdVal != "" {
117+
traceId := strings.TrimSpace(traceIdVal)
118+
content, err := k.TraceDetails(params.Context, traceId)
119+
if err != nil {
120+
return api.NewToolCallResult("", fmt.Errorf("failed to get trace details: %v", err)), nil
121+
}
122+
return api.NewToolCallResult(content, nil), nil
123+
}
124+
125+
// Otherwise, get traces for a resource (existing functionality)
108126
resourceType, _ := params.GetArguments()["resource_type"].(string)
109127
namespace, _ := params.GetArguments()["namespace"].(string)
110128
resourceName, _ := params.GetArguments()["resource_name"].(string)
@@ -114,34 +132,78 @@ func TracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
114132
resourceName = strings.TrimSpace(resourceName)
115133

116134
if resourceType == "" {
117-
return api.NewToolCallResult("", fmt.Errorf("resource_type is required")), nil
135+
return api.NewToolCallResult("", fmt.Errorf("resource_type is required when traceId is not provided")), nil
118136
}
119137
if namespace == "" || len(strings.Split(namespace, ",")) != 1 {
120-
return api.NewToolCallResult("", fmt.Errorf("namespace is required")), nil
138+
return api.NewToolCallResult("", fmt.Errorf("namespace is required when traceId is not provided")), nil
121139
}
122140
if resourceName == "" {
123-
return api.NewToolCallResult("", fmt.Errorf("resource_name is required")), nil
141+
return api.NewToolCallResult("", fmt.Errorf("resource_name is required when traceId is not provided")), nil
124142
}
125143

126-
k := params.NewKiali()
127-
128144
ops, ok := tracesOpsMap[resourceType]
129145
if !ok {
130146
return api.NewToolCallResult("", fmt.Errorf("invalid resource type: %s", resourceType)), nil
131147
}
132148

133149
queryParams := make(map[string]string)
134-
if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" {
135-
queryParams["startMicros"] = startMicros
150+
151+
// Handle startMicros: if not provided, default to 10 minutes ago
152+
var startMicros string
153+
if startMicrosVal, ok := params.GetArguments()["startMicros"].(string); ok && startMicrosVal != "" {
154+
startMicros = startMicrosVal
155+
} else {
156+
// Default to 10 minutes before current time
157+
now := time.Now()
158+
tenMinutesAgo := now.Add(-10 * time.Minute)
159+
startMicros = strconv.FormatInt(tenMinutesAgo.UnixMicro(), 10)
136160
}
137-
if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" {
138-
queryParams["endMicros"] = endMicros
161+
queryParams["startMicros"] = startMicros
162+
163+
// Handle endMicros: if not provided, default to 10 minutes after startMicros
164+
var endMicros string
165+
if endMicrosVal, ok := params.GetArguments()["endMicros"].(string); ok && endMicrosVal != "" {
166+
endMicros = endMicrosVal
167+
} else {
168+
// Parse startMicros to calculate endMicros
169+
startMicrosInt, err := strconv.ParseInt(startMicros, 10, 64)
170+
if err != nil {
171+
return api.NewToolCallResult("", fmt.Errorf("invalid startMicros value: %v", err)), nil
172+
}
173+
startTime := time.UnixMicro(startMicrosInt)
174+
endTime := startTime.Add(10 * time.Minute)
175+
endMicros = strconv.FormatInt(endTime.UnixMicro(), 10)
139176
}
140-
if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" {
141-
queryParams["limit"] = limit
177+
queryParams["endMicros"] = endMicros
178+
// Handle limit: convert integer to string if provided
179+
if limit := params.GetArguments()["limit"]; limit != nil {
180+
switch v := limit.(type) {
181+
case float64:
182+
queryParams["limit"] = fmt.Sprintf("%.0f", v)
183+
case int:
184+
queryParams["limit"] = fmt.Sprintf("%d", v)
185+
case int64:
186+
queryParams["limit"] = fmt.Sprintf("%d", v)
187+
case string:
188+
if v != "" {
189+
queryParams["limit"] = v
190+
}
191+
}
142192
}
143-
if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" {
144-
queryParams["minDuration"] = minDuration
193+
// Handle minDuration: convert integer to string if provided
194+
if minDuration := params.GetArguments()["minDuration"]; minDuration != nil {
195+
switch v := minDuration.(type) {
196+
case float64:
197+
queryParams["minDuration"] = fmt.Sprintf("%.0f", v)
198+
case int:
199+
queryParams["minDuration"] = fmt.Sprintf("%d", v)
200+
case int64:
201+
queryParams["minDuration"] = fmt.Sprintf("%d", v)
202+
case string:
203+
if v != "" {
204+
queryParams["minDuration"] = v
205+
}
206+
}
145207
}
146208
if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" {
147209
queryParams["tags"] = tags

0 commit comments

Comments
 (0)