Skip to content

Commit 64ffa91

Browse files
committed
fix collector api endpoints and add subcommands
1 parent c3d3009 commit 64ffa91

File tree

29 files changed

+1644
-358
lines changed

29 files changed

+1644
-358
lines changed

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ require (
2020
github.com/gorilla/handlers v1.5.2
2121
github.com/gorilla/mux v1.8.1
2222
github.com/gosnmp/gosnmp v1.42.1
23+
github.com/grafana/pyroscope-go v1.2.7
2324
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
2425
github.com/guptarohit/asciigraph v0.7.3
2526
github.com/hairyhenderson/gomplate/v3 v3.11.8
27+
github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce
2628
github.com/hashicorp/consul/api v1.32.0
2729
github.com/hashicorp/go-plugin v1.7.0
2830
github.com/hashicorp/golang-lru/v2 v2.0.7
@@ -122,10 +124,10 @@ require (
122124
github.com/google/gofuzz v1.2.0 // indirect
123125
github.com/google/s2a-go v0.1.9 // indirect
124126
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
127+
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
125128
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
126129
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
127130
github.com/hairyhenderson/go-fsimpl v0.0.0-20220529183339-9deae3e35047 // indirect
128-
github.com/hairyhenderson/yaml v0.0.0-20220618171115-2d35fca545ce // indirect
129131
github.com/hashicorp/go-msgpack v1.1.5 // indirect
130132
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect
131133
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,10 @@ github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6
610610
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
611611
github.com/gosnmp/gosnmp v1.42.1 h1:MEJxhpC5v1coL3tFRix08PYmky9nyb1TLRRgJAmXm8A=
612612
github.com/gosnmp/gosnmp v1.42.1/go.mod h1:CxVS6bXqmWZlafUj9pZUnQX5e4fAltqPcijxWpCitDo=
613+
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
614+
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
615+
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
616+
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
613617
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
614618
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
615619
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=

pkg/api/target/subscribe.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ func (t *Target) ReadSubscriptions() (chan *SubscribeResponse, chan *TargetError
494494

495495
func (t *Target) NumberOfOnceSubscriptions() int {
496496
num := 0
497+
t.m.Lock()
498+
defer t.m.Unlock()
497499
for _, sub := range t.Subscriptions {
498500
if strings.ToUpper(sub.Mode) == "ONCE" {
499501
num++

pkg/cmd/collector/collector.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ func New(gApp *app.App) *cobra.Command {
3535
},
3636
SilenceUsage: true,
3737
}
38+
c.InitCollectorFlags(cmd)
3839
cmd.AddCommand(newCollectorTargetsCmd(gApp))
3940
cmd.AddCommand(newCollectorSubscriptionsCmd(gApp))
4041
cmd.AddCommand(newCollectorOutputsCmd(gApp))
42+
cmd.AddCommand(newCollectorProcessorsCmd(gApp))
43+
cmd.AddCommand(newCollectorInputsCmd(gApp))
4144
return cmd
4245
}
4346

@@ -73,6 +76,7 @@ func getAPIServerClient(store store.Store[any]) (*http.Client, error) {
7376
}
7477
if apiCfg.TLS != nil {
7578
return &http.Client{
79+
Timeout: apiCfg.Timeout,
7680
Transport: &http.Transport{
7781
TLSClientConfig: &tls.Config{
7882
InsecureSkipVerify: true,

pkg/cmd/collector/inputs.go

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
// © 2025 Nokia.
2+
//
3+
// This code is a Contribution to the gNMIc project (“Work”) made under the Google Software Grant and Corporate Contributor License Agreement (“CLA”) and governed by the Apache License 2.0.
4+
// No other rights or licenses in or to any of Nokia’s intellectual property are granted for any other purpose.
5+
// This code is provided on an “as is” basis without any warranties of any kind.
6+
//
7+
// SPDX-License-Identifier: Apache-2.0
8+
9+
package collector
10+
11+
import (
12+
"bytes"
13+
"encoding/json"
14+
"fmt"
15+
"io"
16+
"net/http"
17+
"os"
18+
"sort"
19+
20+
"github.com/olekukonko/tablewriter"
21+
"github.com/openconfig/gnmic/pkg/app"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
func newCollectorInputsCmd(gApp *app.App) *cobra.Command {
26+
cmd := &cobra.Command{
27+
Use: "inputs",
28+
Aliases: []string{"input", "in"},
29+
Short: "manage inputs",
30+
SilenceUsage: true,
31+
}
32+
cmd.AddCommand(
33+
newCollectorInputsListCmd(gApp),
34+
newCollectorInputsGetCmd(gApp),
35+
newCollectorInputsSetCmd(gApp),
36+
newCollectorInputsDeleteCmd(gApp),
37+
)
38+
return cmd
39+
}
40+
41+
func newCollectorInputsListCmd(gApp *app.App) *cobra.Command {
42+
cmd := &cobra.Command{
43+
Use: "list",
44+
Aliases: []string{"ls"},
45+
Short: "list inputs",
46+
SilenceUsage: true,
47+
RunE: func(cmd *cobra.Command, args []string) error {
48+
apiURL, err := getAPIServerURL(gApp.Store)
49+
if err != nil {
50+
return err
51+
}
52+
client, err := getAPIServerClient(gApp.Store)
53+
if err != nil {
54+
return err
55+
}
56+
resp, err := client.Get(apiURL + "/api/v1/config/inputs")
57+
if err != nil {
58+
return err
59+
}
60+
defer resp.Body.Close()
61+
tb, err := io.ReadAll(resp.Body)
62+
if err != nil {
63+
return err
64+
}
65+
if resp.StatusCode != http.StatusOK {
66+
return fmt.Errorf("failed to list inputs, status code: %d: %s", resp.StatusCode, string(tb))
67+
}
68+
69+
// Parse the response as array of maps
70+
inputsResponse := make(map[string]interface{}, 0)
71+
err = json.Unmarshal(tb, &inputsResponse)
72+
if err != nil {
73+
return err
74+
}
75+
76+
// if len(outputsResponse) == 0 {
77+
// fmt.Println("No outputs found")
78+
// return nil
79+
// }
80+
inputs := make([]map[string]interface{}, 0)
81+
for name, input := range inputsResponse {
82+
switch input := input.(type) {
83+
case map[string]any:
84+
input["name"] = name
85+
inputs = append(inputs, input)
86+
default:
87+
return fmt.Errorf("unknown input type: %T", input)
88+
}
89+
}
90+
// Display as horizontal table
91+
table := tablewriter.NewWriter(os.Stdout)
92+
table.SetHeader([]string{"Name", "Type", "Format", "Event Processors"})
93+
table.SetAutoWrapText(false)
94+
table.SetAutoFormatHeaders(true)
95+
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
96+
table.SetAlignment(tablewriter.ALIGN_LEFT)
97+
table.SetCenterSeparator("")
98+
table.SetColumnSeparator("")
99+
table.SetRowSeparator("")
100+
table.SetHeaderLine(false)
101+
table.SetBorder(false)
102+
table.SetTablePadding("\t")
103+
table.SetNoWhiteSpace(true)
104+
105+
data := tableFormatInputsList(inputs)
106+
table.AppendBulk(data)
107+
table.Render()
108+
109+
return nil
110+
},
111+
}
112+
return cmd
113+
}
114+
115+
func newCollectorInputsGetCmd(gApp *app.App) *cobra.Command {
116+
cmd := &cobra.Command{
117+
Use: "get",
118+
Aliases: []string{"g", "show", "sh"},
119+
Short: "get an input",
120+
SilenceUsage: true,
121+
RunE: func(cmd *cobra.Command, args []string) error {
122+
name, err := cmd.Flags().GetString("name")
123+
if err != nil {
124+
return err
125+
}
126+
if name == "" {
127+
return fmt.Errorf("input name is required")
128+
}
129+
apiURL, err := getAPIServerURL(gApp.Store)
130+
if err != nil {
131+
return err
132+
}
133+
client, err := getAPIServerClient(gApp.Store)
134+
if err != nil {
135+
return err
136+
}
137+
resp, err := client.Get(apiURL + "/api/v1/config/inputs/" + name)
138+
if err != nil {
139+
return err
140+
}
141+
defer resp.Body.Close()
142+
tb, err := io.ReadAll(resp.Body)
143+
if err != nil {
144+
return err
145+
}
146+
if resp.StatusCode != http.StatusOK {
147+
return fmt.Errorf("failed to get input, status code: %d: %s", resp.StatusCode, string(tb))
148+
}
149+
150+
// Parse the response as a map
151+
input := make(map[string]interface{})
152+
err = json.Unmarshal(tb, &input)
153+
if err != nil {
154+
return err
155+
}
156+
157+
// Display as vertical table (key-value pairs)
158+
table := tablewriter.NewWriter(os.Stdout)
159+
table.SetHeader([]string{"PARAM", "VALUE"})
160+
table.SetAutoWrapText(false)
161+
table.SetAutoFormatHeaders(false)
162+
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
163+
table.SetAlignment(tablewriter.ALIGN_LEFT)
164+
table.SetCenterSeparator("")
165+
table.SetColumnSeparator(":")
166+
table.SetRowSeparator("")
167+
table.SetHeaderLine(false)
168+
table.SetBorder(false)
169+
table.SetTablePadding("\t")
170+
table.SetNoWhiteSpace(true)
171+
table.SetColumnAlignment([]int{tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT})
172+
173+
data := tableFormatInputVertical(input)
174+
table.AppendBulk(data)
175+
table.Render()
176+
177+
return nil
178+
},
179+
}
180+
cmd.Flags().StringP("name", "n", "", "input name")
181+
return cmd
182+
}
183+
184+
func newCollectorInputsSetCmd(gApp *app.App) *cobra.Command {
185+
cmd := &cobra.Command{
186+
Use: "set",
187+
Aliases: []string{"create", "cr"},
188+
Short: "set an input",
189+
SilenceUsage: true,
190+
RunE: func(cmd *cobra.Command, args []string) error {
191+
inputConfigFile, err := cmd.Flags().GetString("input")
192+
if err != nil {
193+
return err
194+
}
195+
if inputConfigFile == "" {
196+
return fmt.Errorf("input file is required")
197+
}
198+
b, err := os.ReadFile(inputConfigFile)
199+
if err != nil {
200+
return err
201+
}
202+
var inputConfig map[string]interface{}
203+
err = json.Unmarshal(b, &inputConfig)
204+
if err != nil {
205+
return err
206+
}
207+
client, err := getAPIServerClient(gApp.Store)
208+
if err != nil {
209+
return err
210+
}
211+
apiURL, err := getAPIServerURL(gApp.Store)
212+
if err != nil {
213+
return err
214+
}
215+
resp, err := client.Post(apiURL+"/api/v1/config/inputs", "application/json", bytes.NewBuffer(b))
216+
if err != nil {
217+
return err
218+
}
219+
defer resp.Body.Close()
220+
221+
if resp.StatusCode != http.StatusOK {
222+
tb, _ := io.ReadAll(resp.Body)
223+
return fmt.Errorf("failed to create input, status code: %d: %s", resp.StatusCode, string(tb))
224+
}
225+
226+
inputName := formatValue(inputConfig["name"])
227+
fmt.Fprintf(os.Stderr, "Input '%s' created successfully\n", inputName)
228+
return nil
229+
},
230+
}
231+
cmd.Flags().StringP("input", "i", "", "input config file")
232+
return cmd
233+
}
234+
235+
func newCollectorInputsDeleteCmd(gApp *app.App) *cobra.Command {
236+
cmd := &cobra.Command{
237+
Use: "delete",
238+
Aliases: []string{"d", "del", "rm"},
239+
Short: "delete an input",
240+
SilenceUsage: true,
241+
RunE: func(cmd *cobra.Command, args []string) error {
242+
name, err := cmd.Flags().GetString("name")
243+
if err != nil {
244+
return err
245+
}
246+
if name == "" {
247+
return fmt.Errorf("input name is required")
248+
}
249+
apiURL, err := getAPIServerURL(gApp.Store)
250+
if err != nil {
251+
return err
252+
}
253+
client, err := getAPIServerClient(gApp.Store)
254+
if err != nil {
255+
return err
256+
}
257+
req, err := http.NewRequest(http.MethodDelete, apiURL+"/api/v1/config/inputs/"+name, nil)
258+
if err != nil {
259+
return err
260+
}
261+
resp, err := client.Do(req)
262+
if err != nil {
263+
return err
264+
}
265+
defer resp.Body.Close()
266+
267+
if resp.StatusCode != http.StatusOK {
268+
tb, _ := io.ReadAll(resp.Body)
269+
return fmt.Errorf("failed to delete input, status code: %d: %s", resp.StatusCode, string(tb))
270+
}
271+
272+
fmt.Fprintln(os.Stderr, "Input deleted successfully")
273+
return nil
274+
},
275+
}
276+
cmd.Flags().StringP("name", "n", "", "input name")
277+
return cmd
278+
}
279+
280+
// tableFormatOutputVertical formats a single output as vertical table (key-value pairs)
281+
func tableFormatInputVertical(input map[string]any) [][]string {
282+
data := make([][]string, 0)
283+
284+
// Sort keys for consistent output
285+
keys := make([]string, 0, len(input))
286+
for k := range input {
287+
keys = append(keys, k)
288+
}
289+
sort.Strings(keys)
290+
291+
// Add each key-value pair
292+
for _, key := range keys {
293+
value := input[key]
294+
formattedValue := formatValue(value)
295+
data = append(data, []string{key, formattedValue})
296+
}
297+
298+
return data
299+
}
300+
301+
// tableFormatInputsList formats multiple outputs as horizontal table (summary view)
302+
func tableFormatInputsList(inputs []map[string]any) [][]string {
303+
data := make([][]string, 0, len(inputs))
304+
305+
for _, input := range inputs {
306+
name := formatValue(input["name"])
307+
inputType := formatValue(input["type"])
308+
format := formatValue(input["format"])
309+
310+
// Handle event-processors
311+
eventProcessors := "-"
312+
if ep, ok := input["event-processors"]; ok {
313+
eventProcessors = formatValueShort(ep)
314+
}
315+
316+
data = append(data, []string{
317+
name,
318+
inputType,
319+
format,
320+
eventProcessors,
321+
})
322+
}
323+
324+
// Sort by name
325+
sort.Slice(data, func(i, j int) bool {
326+
return data[i][0] < data[j][0]
327+
})
328+
329+
return data
330+
}

0 commit comments

Comments
 (0)