Skip to content

Commit 5be45c6

Browse files
authored
Merge pull request #138 from StackVista/topology-state
Query Topology Health states
2 parents e8c7ab8 + a3a9f8f commit 5be45c6

File tree

5 files changed

+494
-41
lines changed

5 files changed

+494
-41
lines changed

cmd/topology.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func TopologyCommand(cli *di.Deps) *cobra.Command {
1313
Long: "Inspect SUSE Observability topology components. Query and display topology components using component types, tags, and identifiers.",
1414
}
1515
cmd.AddCommand(topology.InspectCommand(cli))
16+
cmd.AddCommand(topology.StateCommand(cli))
1617

1718
return cmd
1819
}

cmd/topology/topology_inspect_test.go

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,47 +18,6 @@ func setInspectCmd(t *testing.T) (*di.MockDeps, *cobra.Command) {
1818
return &cli, cmd
1919
}
2020

21-
func mockSnapshotResponse() sts.QuerySnapshotResult {
22-
return sts.QuerySnapshotResult{
23-
Type: "QuerySnapshotResult",
24-
ViewSnapshotResponse: map[string]interface{}{
25-
"_type": "ViewSnapshot",
26-
"components": []interface{}{
27-
map[string]interface{}{
28-
"id": float64(229404307680647),
29-
"name": "test-component",
30-
"type": float64(239975151751041),
31-
"layer": float64(186771622698247),
32-
"domain": float64(209616858431909),
33-
"identifiers": []interface{}{"urn:test:component:1"},
34-
"tags": []interface{}{"service.namespace:test"},
35-
"properties": map[string]interface{}{"key": "value"},
36-
},
37-
},
38-
"metadata": map[string]interface{}{
39-
"componentTypes": []interface{}{
40-
map[string]interface{}{
41-
"id": float64(239975151751041),
42-
"name": "test type",
43-
},
44-
},
45-
"layers": []interface{}{
46-
map[string]interface{}{
47-
"id": float64(186771622698247),
48-
"name": "Test Layer",
49-
},
50-
},
51-
"domains": []interface{}{
52-
map[string]interface{}{
53-
"id": float64(209616858431909),
54-
"name": "Test Domain",
55-
},
56-
},
57-
},
58-
},
59-
}
60-
}
61-
6221
func TestTopologyInspectJson(t *testing.T) {
6322
cli, cmd := setInspectCmd(t)
6423
cli.MockClient.ApiMocks.SnapshotApi.QuerySnapshotResponse.Result = mockSnapshotResponse()

cmd/topology/topology_state.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package topology
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/stackvista/stackstate-cli/generated/stackstate_api"
9+
"github.com/stackvista/stackstate-cli/internal/common"
10+
"github.com/stackvista/stackstate-cli/internal/di"
11+
"github.com/stackvista/stackstate-cli/internal/printer"
12+
)
13+
14+
type StateArgs struct {
15+
ComponentType string
16+
Tags []string
17+
Identifiers []string
18+
Limit int
19+
}
20+
21+
func StateCommand(cli *di.Deps) *cobra.Command {
22+
args := &StateArgs{}
23+
24+
cmd := &cobra.Command{
25+
Use: "state",
26+
Short: "Show the health state of topology components",
27+
Long: "Show the health state of topology components by type, tags, and identifiers. Displays the health state for each matching component.",
28+
Example: `# show state of components of a specific type
29+
sts topology state --type "otel service instance"
30+
31+
# show state with tag filtering
32+
sts topology state --type "otel service instance" --tag "service.namespace:opentelemetry-demo-demo-dev"
33+
34+
# show state with multiple tags (ANDed)
35+
sts topology state --type "otel service instance" \
36+
--tag "service.namespace:opentelemetry-demo-demo-dev" \
37+
--tag "service.name:accountingservice"
38+
39+
# show state with identifier filtering
40+
sts topology state --type "otel service instance" --identifier "urn:opentelemetry:..."
41+
42+
# show state with limit on number of results
43+
sts topology state --type "otel service instance" --limit 10
44+
45+
# show state and display as JSON
46+
sts topology state --type "otel service instance" -o json`,
47+
RunE: cli.CmdRunEWithApi(func(cmd *cobra.Command, cli *di.Deps, api *stackstate_api.APIClient, serverInfo *stackstate_api.ServerInfo) common.CLIError {
48+
return RunStateCommand(cmd, cli, api, serverInfo, args)
49+
}),
50+
}
51+
52+
cmd.Flags().StringVar(&args.ComponentType, "type", "", "Component type")
53+
cmd.Flags().StringSliceVar(&args.Tags, "tag", []string{}, "Filter by tags in format 'tag-name:tag-value' (multiple allowed, ANDed together)")
54+
cmd.Flags().StringSliceVar(&args.Identifiers, "identifier", []string{}, "Filter by component identifiers (multiple allowed, ANDed together)")
55+
cmd.Flags().IntVar(&args.Limit, "limit", 0, "Maximum number of components to output (must be positive)")
56+
57+
return cmd
58+
}
59+
60+
func RunStateCommand(
61+
_ *cobra.Command,
62+
cli *di.Deps,
63+
api *stackstate_api.APIClient,
64+
_ *stackstate_api.ServerInfo,
65+
args *StateArgs,
66+
) common.CLIError {
67+
if args.Limit < 0 {
68+
return common.NewExecutionError(fmt.Errorf("limit must be a positive number, got: %d", args.Limit))
69+
}
70+
71+
query := buildSTQLQuery(args.ComponentType, args.Tags, args.Identifiers)
72+
73+
metadata := stackstate_api.NewQueryMetadata(
74+
false,
75+
false,
76+
0,
77+
false,
78+
false,
79+
false,
80+
false,
81+
false,
82+
false,
83+
true,
84+
)
85+
86+
request := stackstate_api.NewViewSnapshotRequest(
87+
"SnapshotRequest",
88+
query,
89+
"0.0.1",
90+
*metadata,
91+
)
92+
93+
result, resp, err := api.SnapshotApi.QuerySnapshot(cli.Context).
94+
ViewSnapshotRequest(*request).
95+
Execute()
96+
if err != nil {
97+
return common.NewResponseError(err, resp)
98+
}
99+
100+
componentStates, parseErr := parseStateResponse(result)
101+
if parseErr != nil {
102+
if typedErr := handleSnapshotError(result.ViewSnapshotResponse, resp); typedErr != nil {
103+
return typedErr
104+
}
105+
return common.NewExecutionError(parseErr)
106+
}
107+
108+
// Apply limit if specified
109+
if args.Limit > 0 && len(componentStates) > args.Limit {
110+
componentStates = componentStates[:args.Limit]
111+
}
112+
113+
if cli.IsJson() {
114+
cli.Printer.PrintJson(map[string]interface{}{
115+
"components": componentStates,
116+
})
117+
return nil
118+
} else {
119+
printStateTableOutput(cli, componentStates)
120+
}
121+
122+
return nil
123+
}
124+
125+
// ComponentState holds the component information along with its health state.
126+
type ComponentState struct {
127+
ID int64 `json:"id"`
128+
Name string `json:"name"`
129+
Type string `json:"type"`
130+
Identifiers []string `json:"identifiers"`
131+
HealthState string `json:"healthState"`
132+
}
133+
134+
func parseStateResponse(result *stackstate_api.QuerySnapshotResult) ([]ComponentState, error) {
135+
respMap := result.ViewSnapshotResponse
136+
if respMap == nil {
137+
return nil, fmt.Errorf("response data is nil")
138+
}
139+
140+
respType, ok := respMap["_type"].(string)
141+
if !ok {
142+
return nil, fmt.Errorf("response has no _type discriminator")
143+
}
144+
145+
if respType != "ViewSnapshot" {
146+
return nil, fmt.Errorf("response is an error type: %s", respType)
147+
}
148+
149+
metadata := parseMetadata(respMap)
150+
151+
var componentStates []ComponentState
152+
if componentsSlice, ok := respMap["components"].([]interface{}); ok {
153+
for _, comp := range componentsSlice {
154+
if compMap, ok := comp.(map[string]interface{}); ok {
155+
componentStates = append(componentStates, parseComponentStateFromMap(compMap, metadata))
156+
}
157+
}
158+
}
159+
160+
return componentStates, nil
161+
}
162+
163+
func parseComponentStateFromMap(compMap map[string]interface{}, metadata ComponentMetadata) ComponentState {
164+
cs := ComponentState{
165+
Identifiers: []string{},
166+
HealthState: "UNKNOWN",
167+
}
168+
169+
// Parse basic fields
170+
if id, ok := compMap["id"].(float64); ok {
171+
cs.ID = int64(id)
172+
}
173+
if name, ok := compMap["name"].(string); ok {
174+
cs.Name = name
175+
}
176+
177+
// Parse type
178+
if typeID, ok := compMap["type"].(float64); ok {
179+
if typeName, found := metadata.ComponentTypes[int64(typeID)]; found {
180+
cs.Type = typeName
181+
} else {
182+
cs.Type = fmt.Sprintf("Unknown (%d)", int64(typeID))
183+
}
184+
}
185+
186+
// Parse identifiers
187+
if identifiersRaw, ok := compMap["identifiers"].([]interface{}); ok {
188+
for _, idRaw := range identifiersRaw {
189+
if id, ok := idRaw.(string); ok {
190+
cs.Identifiers = append(cs.Identifiers, id)
191+
}
192+
}
193+
}
194+
195+
// Parse health state from state.healthState
196+
if stateMap, ok := compMap["state"].(map[string]interface{}); ok {
197+
if healthState, ok := stateMap["healthState"].(string); ok {
198+
cs.HealthState = healthState
199+
}
200+
}
201+
202+
return cs
203+
}
204+
205+
func printStateTableOutput(cli *di.Deps, componentStates []ComponentState) {
206+
var tableData [][]interface{}
207+
for _, cs := range componentStates {
208+
identifiersStr := strings.Join(cs.Identifiers, ", ")
209+
tableData = append(tableData, []interface{}{
210+
cs.Name,
211+
cs.Type,
212+
cs.HealthState,
213+
identifiersStr,
214+
})
215+
}
216+
217+
cli.Printer.Table(printer.TableData{
218+
Header: []string{"Name", "Type", "Health State", "Identifiers"},
219+
Data: tableData,
220+
MissingTableDataMsg: printer.NotFoundMsg{Types: "components"},
221+
})
222+
}

0 commit comments

Comments
 (0)