|
| 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