diff --git a/cmd/about.go b/cmd/about.go deleted file mode 100644 index 7271bd4..0000000 --- a/cmd/about.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2024 Parseable, Inc -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package cmd - -import ( - "encoding/json" - "errors" - "fmt" - "io" -) - -// About struct -type About struct { - Version string `json:"version"` - UIVersion string `json:"uiVersion"` - Commit string `json:"commit"` - DeploymentID string `json:"deploymentId"` - UpdateAvailable bool `json:"updateAvailable"` - LatestVersion string `json:"latestVersion"` - LLMActive bool `json:"llmActive"` - LLMProvider string `json:"llmProvider"` - OIDCActive bool `json:"oidcActive"` - License string `json:"license"` - Mode string `json:"mode"` - Staging string `json:"staging"` - HotTier string `json:"hotTier"` - GRPCPort int `json:"grpcPort"` - Store Store `json:"store"` - Analytics Analytics `json:"analytics"` - QueryEngine string `json:"queryEngine"` -} - -// Store struct -type Store struct { - Type string `json:"type"` - Path string `json:"path"` -} - -// Analytics struct -type Analytics struct { - ClarityTag string `json:"clarityTag"` -} - -func FetchAbout(client *HTTPClient) (about About, err error) { - req, err := client.NewRequest("GET", "about", nil) - if err != nil { - return - } - - resp, err := client.client.Do(req) - if err != nil { - return - } - - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - err = json.Unmarshal(bytes, &about) - } else { - body := string(bytes) - body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - err = errors.New(body) - } - return -} diff --git a/cmd/profile.go b/cmd/profile.go index a46e8b4..3efc30f 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -20,14 +20,12 @@ import ( "errors" "fmt" "net/url" - "os" "pb/pkg/config" "pb/pkg/model/credential" "pb/pkg/model/defaultprofile" + "time" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/muesli/termenv" "github.com/spf13/cobra" ) @@ -60,11 +58,12 @@ var outputFormat string // Initialize flags func init() { - AddProfileCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format (text|json)") - RemoveProfileCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format (text|json)") - DefaultProfileCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format (text|json)") - ListProfileCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format (text|json)") + AddProfileCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format (text|json)") + RemoveProfileCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format (text|json)") + DefaultProfileCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format (text|json)") + ListProfileCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format (text|json)") } + func outputResult(v interface{}) error { if outputFormat == "json" { jsonData, err := json.MarshalIndent(v, "", " ") @@ -89,68 +88,67 @@ var AddProfileCmd = &cobra.Command{ } return cobra.MaximumNArgs(4)(cmd, args) }, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + startTime := time.Now() + var commandError error + + // Parsing input and handling errors name := args[0] url, err := url.Parse(args[1]) if err != nil { - return err + commandError = fmt.Errorf("error parsing URL: %s", err) + cmd.Annotations["error"] = commandError.Error() + return commandError } - var username string - var password string - + var username, password string if len(args) < 4 { _m, err := tea.NewProgram(credential.New()).Run() if err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) + commandError = fmt.Errorf("error reading credentials: %s", err) + cmd.Annotations["error"] = commandError.Error() + return commandError } m := _m.(credential.Model) - username, password = m.Values() } else { username = args[2] password = args[3] } - profile := config.Profile{ - URL: url.String(), - Username: username, - Password: password, - } - + profile := config.Profile{URL: url.String(), Username: username, Password: password} fileConfig, err := config.ReadConfigFromFile() if err != nil { - // create new file newConfig := config.Config{ - Profiles: map[string]config.Profile{ - name: profile, - }, + Profiles: map[string]config.Profile{name: profile}, DefaultProfile: name, } err = config.WriteConfigToFile(&newConfig) - return err - } - if fileConfig.Profiles == nil { - fileConfig.Profiles = make(map[string]config.Profile) - } - fileConfig.Profiles[name] = profile - if fileConfig.DefaultProfile == "" { - fileConfig.DefaultProfile = name + commandError = err + } else { + if fileConfig.Profiles == nil { + fileConfig.Profiles = make(map[string]config.Profile) + } + fileConfig.Profiles[name] = profile + if fileConfig.DefaultProfile == "" { + fileConfig.DefaultProfile = name + } + commandError = config.WriteConfigToFile(fileConfig) } - err = config.WriteConfigToFile(fileConfig) - if err != nil { - fmt.Printf("add profile %s failed\n, err: %v\n", StyleBold.Render(name), err) - return err + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if commandError != nil { + cmd.Annotations["error"] = commandError.Error() + return commandError } - fmt.Printf("Added profile %s\n", StyleBold.Render(name)) if outputFormat == "json" { return outputResult(profile) } fmt.Printf("Profile %s added successfully\n", name) - return nil }, } @@ -161,29 +159,43 @@ var RemoveProfileCmd = &cobra.Command{ Example: " pb profile remove local_parseable", Args: cobra.ExactArgs(1), Short: "Delete a profile", - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + startTime := time.Now() + name := args[0] fileConfig, err := config.ReadConfigFromFile() if err != nil { - return nil + cmd.Annotations["error"] = fmt.Sprintf("error reading config: %s", err) + return err } _, exists := fileConfig.Profiles[name] - if exists { - delete(fileConfig.Profiles, name) - if len(fileConfig.Profiles) == 0 { - fileConfig.DefaultProfile = "" - } + if !exists { + msg := fmt.Sprintf("No profile found with the name: %s", name) + cmd.Annotations["error"] = msg + fmt.Println(msg) + return nil + } - config.WriteConfigToFile(fileConfig) - if outputFormat == "json" { - return outputResult(fmt.Sprintf("Deleted profile %s", name)) - } - fmt.Printf("Deleted profile %s\n", StyleBold.Render(name)) - } else { - fmt.Printf("No profile found with the name: %s", StyleBold.Render(name)) + delete(fileConfig.Profiles, name) + if len(fileConfig.Profiles) == 0 { + fileConfig.DefaultProfile = "" } + commandError := config.WriteConfigToFile(fileConfig) + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if commandError != nil { + cmd.Annotations["error"] = commandError.Error() + return commandError + } + + if outputFormat == "json" { + return outputResult(fmt.Sprintf("Deleted profile %s", name)) + } + fmt.Printf("Deleted profile %s\n", name) return nil }, } @@ -193,46 +205,54 @@ var DefaultProfileCmd = &cobra.Command{ Args: cobra.MaximumNArgs(1), Short: "Set default profile to use with all commands", Example: " pb profile default local_parseable", - RunE: func(_ *cobra.Command, args []string) error { - var name string + RunE: func(cmd *cobra.Command, args []string) error { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + startTime := time.Now() fileConfig, err := config.ReadConfigFromFile() if err != nil { - return nil + cmd.Annotations["error"] = fmt.Sprintf("error reading config: %s", err) + return err } + var name string if len(args) > 0 { name = args[0] } else { model := defaultprofile.New(fileConfig.Profiles) _m, err := tea.NewProgram(model).Run() if err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) + cmd.Annotations["error"] = fmt.Sprintf("error selecting default profile: %s", err) + return err } m := _m.(defaultprofile.Model) - termenv.DefaultOutput().ClearLines(lipgloss.Height(model.View()) - 1) - if m.Success { - name = m.Choice - } else { + if !m.Success { return nil } + name = m.Choice } _, exists := fileConfig.Profiles[name] - if exists { - fileConfig.DefaultProfile = name - } else { - name = lipgloss.NewStyle().Bold(true).Render(name) - err := fmt.Sprintf("profile %s does not exist", StyleBold.Render(name)) - return errors.New(err) + if !exists { + commandError := fmt.Sprintf("profile %s does not exist", name) + cmd.Annotations["error"] = commandError + return errors.New(commandError) + } + + fileConfig.DefaultProfile = name + commandError := config.WriteConfigToFile(fileConfig) + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if commandError != nil { + cmd.Annotations["error"] = commandError.Error() + return commandError } - config.WriteConfigToFile(fileConfig) if outputFormat == "json" { return outputResult(fmt.Sprintf("%s is now set as default profile", name)) } - fmt.Printf("%s is now set as default profile\n", StyleBold.Render(name)) + fmt.Printf("%s is now set as default profile\n", name) return nil }, } @@ -241,27 +261,34 @@ var ListProfileCmd = &cobra.Command{ Use: "list profiles", Short: "List all added profiles", Example: " pb profile list", - RunE: func(_ *cobra.Command, _ []string) error { - fileConfig, err := config.ReadConfigFromFile() - if err != nil { - return nil + RunE: func(cmd *cobra.Command, _ []string) error { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) } + startTime := time.Now() - if len(fileConfig.Profiles) != 0 { - println() + fileConfig, err := config.ReadConfigFromFile() + if err != nil { + cmd.Annotations["error"] = fmt.Sprintf("error reading config: %s", err) + return err } if outputFormat == "json" { - return outputResult(fileConfig.Profiles) + commandError := outputResult(fileConfig.Profiles) + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if commandError != nil { + cmd.Annotations["error"] = commandError.Error() + return commandError + } + return nil } - row := 0 for key, value := range fileConfig.Profiles { item := ProfileListItem{key, value.URL, value.Username} fmt.Println(item.Render(fileConfig.DefaultProfile == key)) - row++ - fmt.Println() + fmt.Println() // Add a blank line after each profile } + cmd.Annotations["executionTime"] = time.Since(startTime).String() return nil }, } diff --git a/cmd/query.go b/cmd/query.go index a284af4..49afd93 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -22,11 +22,14 @@ import ( "io" "os" "strings" + "time" // "pb/pkg/model" //! This dependency is required by the interactive flag Do not remove // tea "github.com/charmbracelet/bubbletea" + internalHTTP "pb/pkg/http" + "github.com/spf13/cobra" ) @@ -39,15 +42,7 @@ var ( endFlagShort = "t" defaultEnd = "now" - // // save filter flags - // saveQueryFlag = "save-as" - // saveQueryShort = "s" - // // save filter with time flags - // saveQueryTimeFlag = "with-time" - // saveQueryTimeShort = "w" outputFlag = "output" - // interactiveFlag = "interactive" - // interactiveFlagShort = "i" ) var query = &cobra.Command{ @@ -58,18 +53,26 @@ var query = &cobra.Command{ Args: cobra.MaximumNArgs(1), PreRunE: PreRunDefaultProfile, RunE: func(command *cobra.Command, args []string) error { - var query string + startTime := time.Now() + command.Annotations = map[string]string{ + "startTime": startTime.Format(time.RFC3339), + } + + defer func() { + duration := time.Since(startTime) + command.Annotations["executionTime"] = duration.String() + }() if len(args) == 0 || strings.TrimSpace(args[0]) == "" { - fmt.Println("please enter your query") + fmt.Println("Please enter your query") fmt.Printf("Example:\n pb query run \"select * from frontend\" --from=10m --to=now\n") return nil } - query = args[0] - + query := args[0] start, err := command.Flags().GetString(startFlag) if err != nil { + command.Annotations["error"] = err.Error() return err } if start == "" { @@ -78,60 +81,65 @@ var query = &cobra.Command{ end, err := command.Flags().GetString(endFlag) if err != nil { + command.Annotations["error"] = err.Error() return err } if end == "" { end = defaultEnd } - outputFormat, err := command.Flags().GetString(outputFlag) + outputFormat, err := command.Flags().GetString("output") if err != nil { - return err + command.Annotations["error"] = err.Error() + return fmt.Errorf("failed to get 'output' flag: %w", err) } - client := DefaultClient() - return fetchData(&client, query, start, end, outputFormat) + client := internalHTTP.DefaultClient(&DefaultProfile) + err = fetchData(&client, query, start, end, outputFormat) + if err != nil { + command.Annotations["error"] = err.Error() + } + return err }, } -var QueryCmd = func() *cobra.Command { +func init() { query.Flags().StringP(startFlag, startFlagShort, defaultStart, "Start time for query.") query.Flags().StringP(endFlag, endFlagShort, defaultEnd, "End time for query.") - query.Flags().StringP(outputFlag, "o", "text", "Output format (text or json).") - return query -}() + query.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format (text|json)") +} + +var QueryCmd = query -func fetchData(client *HTTPClient, query string, startTime, endTime, outputFormat string) (err error) { +func fetchData(client *internalHTTP.HTTPClient, query string, startTime, endTime, outputFormat string) error { queryTemplate := `{ "query": "%s", "startTime": "%s", "endTime": "%s" }` - finalQuery := fmt.Sprintf(queryTemplate, query, startTime, endTime) req, err := client.NewRequest("POST", "query", bytes.NewBuffer([]byte(finalQuery))) if err != nil { - return + return fmt.Errorf("failed to create new request: %w", err) } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { - return + return fmt.Errorf("request execution failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) fmt.Println(string(body)) - return nil + return fmt.Errorf("non-200 status code received: %s", resp.Status) } if outputFormat == "json" { var jsonResponse []map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&jsonResponse); err != nil { - fmt.Println("Error decoding JSON response:", err) - return err + return fmt.Errorf("error decoding JSON response: %w", err) } encodedResponse, _ := json.MarshalIndent(jsonResponse, "", " ") fmt.Println(string(encodedResponse)) diff --git a/cmd/queryList.go b/cmd/queryList.go index 61ac4b2..a81b196 100644 --- a/cmd/queryList.go +++ b/cmd/queryList.go @@ -22,6 +22,7 @@ import ( "net/http" "os" "pb/pkg/config" + internalHTTP "pb/pkg/http" "pb/pkg/model" "strings" "time" @@ -36,7 +37,7 @@ var SavedQueryList = &cobra.Command{ Long: "\nShow the list of saved queries for active user", PreRunE: PreRunDefaultProfile, Run: func(_ *cobra.Command, _ []string) { - client := DefaultClient() + client := internalHTTP.DefaultClient(&DefaultProfile) // Check if the output flag is set if outputFlag != "" { @@ -64,6 +65,10 @@ var SavedQueryList = &cobra.Command{ fmt.Println("Error converting saved queries to JSON:", err) return } + if string(jsonOutput) == "null" { + fmt.Println("[]") + return + } fmt.Println(string(jsonOutput)) } else { for _, query := range userSavedQueries { @@ -113,7 +118,7 @@ var SavedQueryList = &cobra.Command{ } // Delete a saved query from the list. -func deleteSavedQuery(client *HTTPClient, savedQueryID, title string) { +func deleteSavedQuery(client *internalHTTP.HTTPClient, savedQueryID, title string) { fmt.Printf("\nAttempting to delete '%s'", title) deleteURL := `filters/` + savedQueryID req, err := client.NewRequest("DELETE", deleteURL, nil) @@ -121,7 +126,7 @@ func deleteSavedQuery(client *HTTPClient, savedQueryID, title string) { fmt.Println("Failed to delete the saved query with error: ", err) } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { return } diff --git a/cmd/role.go b/cmd/role.go index 74bc8c6..dbd1cbd 100644 --- a/cmd/role.go +++ b/cmd/role.go @@ -20,10 +20,12 @@ import ( "encoding/json" "fmt" "io" - "os" "pb/pkg/model/role" "strings" "sync" + "time" + + internalHTTP "pb/pkg/http" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -66,15 +68,22 @@ var AddRoleCmd = &cobra.Command{ Example: " pb role add ingestors", Short: "Add a new role", Args: cobra.ExactArgs(1), - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] - // check if the role already exists var roles []string - client := DefaultClient() + client := internalHTTP.DefaultClient(&DefaultProfile) if err := fetchRoles(&client, &roles); err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error fetching roles: %s", err.Error()) return err } + if strings.Contains(strings.Join(roles, " "), name) { fmt.Println("role already exists, please use a different name") return nil @@ -82,11 +91,11 @@ var AddRoleCmd = &cobra.Command{ _m, err := tea.NewProgram(role.New()).Run() if err != nil { - fmt.Printf("there's been an error: %v", err) - os.Exit(1) + cmd.Annotations["errors"] = fmt.Sprintf("Error initializing program: %s", err.Error()) + return err } - m := _m.(role.Model) + m := _m.(role.Model) privilege := m.Selection.Value() stream := m.Stream.Value() tag := m.Tag.Value() @@ -97,28 +106,13 @@ var AddRoleCmd = &cobra.Command{ } var putBody io.Reader - - // set role if privilege != "none" { - roleData := RoleData{ - Privilege: privilege, - } + roleData := RoleData{Privilege: privilege} switch privilege { - case "writer": - roleData.Resource = &RoleResource{ - Stream: stream, - } + case "writer", "ingestor": + roleData.Resource = &RoleResource{Stream: stream} case "reader": - roleData.Resource = &RoleResource{ - Stream: stream, - } - if tag != "" { - roleData.Resource.Tag = tag - } - case "ingestor": - roleData.Resource = &RoleResource{ - Stream: stream, - } + roleData.Resource = &RoleResource{Stream: stream, Tag: tag} } roleDataJSON, _ := json.Marshal([]RoleData{roleData}) putBody = bytes.NewBuffer(roleDataJSON) @@ -126,24 +120,28 @@ var AddRoleCmd = &cobra.Command{ req, err := client.NewRequest("PUT", "role/"+name, putBody) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error creating request: %s", err.Error()) return err } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error performing request: %s", err.Error()) return err } + defer resp.Body.Close() - bytes, err := io.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error reading response: %s", err.Error()) return err } - body := string(bytes) - defer resp.Body.Close() + body := string(bodyBytes) if resp.StatusCode == 200 { fmt.Printf("Added role %s", name) } else { + cmd.Annotations["errors"] = fmt.Sprintf("Request failed - Status: %s, Response: %s", resp.Status, body) fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) } @@ -157,29 +155,38 @@ var RemoveRoleCmd = &cobra.Command{ Example: " pb role remove ingestor", Short: "Delete a role", Args: cobra.ExactArgs(1), - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] - client := DefaultClient() + client := internalHTTP.DefaultClient(&DefaultProfile) req, err := client.NewRequest("DELETE", "role/"+name, nil) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error creating delete request: %s", err.Error()) return err } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error performing delete request: %s", err.Error()) return err } + defer resp.Body.Close() if resp.StatusCode == 200 { fmt.Printf("Removed role %s\n", StyleBold.Render(name)) } else { - bytes, err := io.ReadAll(resp.Body) + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error reading response: %s", err.Error()) return err } - body := string(bytes) - defer resp.Body.Close() - + body := string(bodyBytes) + cmd.Annotations["errors"] = fmt.Sprintf("Request failed - Status: %s, Response: %s", resp.Status, body) fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) } @@ -192,16 +199,23 @@ var ListRoleCmd = &cobra.Command{ Short: "List all roles", Example: " pb role list", RunE: func(cmd *cobra.Command, _ []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + var roles []string - client := DefaultClient() + client := internalHTTP.DefaultClient(&DefaultProfile) err := fetchRoles(&client, &roles) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error fetching roles: %s", err.Error()) return err } - // Get output flag value outputFormat, err := cmd.Flags().GetString("output") if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error retrieving output flag: %s", err.Error()) return err } @@ -210,39 +224,32 @@ var ListRoleCmd = &cobra.Command{ err error }, len(roles)) - wsg := sync.WaitGroup{} + var wg sync.WaitGroup for idx, role := range roles { - wsg.Add(1) - out := &roleResponses[idx] - role := role - client := &client - go func() { - out.data, out.err = fetchSpecificRole(client, role) - wsg.Done() - }() + wg.Add(1) + go func(idx int, role string) { + defer wg.Done() + roleResponses[idx].data, roleResponses[idx].err = fetchSpecificRole(&client, role) + }(idx, role) } + wg.Wait() - wsg.Wait() - - // Output in JSON format if requested if outputFormat == "json" { - // Collect the role data into a structured format allRoles := map[string][]RoleData{} for idx, roleName := range roles { if roleResponses[idx].err == nil { allRoles[roleName] = roleResponses[idx].data } } - // Marshal and print as JSON jsonOutput, err := json.MarshalIndent(allRoles, "", " ") if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error marshaling JSON output: %s", err.Error()) return fmt.Errorf("failed to marshal JSON output: %w", err) } fmt.Println(string(jsonOutput)) return nil } - // Default output in text format fmt.Println() for idx, roleName := range roles { fetchRes := roleResponses[idx] @@ -253,7 +260,8 @@ var ListRoleCmd = &cobra.Command{ fmt.Println(lipgloss.NewStyle().PaddingLeft(3).Render(role.Render())) } } else { - fmt.Println(fetchRes.err) + fmt.Printf("Error fetching role data for %s: %v\n", roleName, fetchRes.err) + cmd.Annotations["errors"] += fmt.Sprintf("Error fetching role data for %s: %v\n", roleName, fetchRes.err) } } @@ -261,13 +269,13 @@ var ListRoleCmd = &cobra.Command{ }, } -func fetchRoles(client *HTTPClient, data *[]string) error { +func fetchRoles(client *internalHTTP.HTTPClient, data *[]string) error { req, err := client.NewRequest("GET", "role", nil) if err != nil { return err } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { return err } @@ -291,13 +299,13 @@ func fetchRoles(client *HTTPClient, data *[]string) error { return nil } -func fetchSpecificRole(client *HTTPClient, role string) (res []RoleData, err error) { +func fetchSpecificRole(client *internalHTTP.HTTPClient, role string) (res []RoleData, err error) { req, err := client.NewRequest("GET", fmt.Sprintf("role/%s", role), nil) if err != nil { return } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { return } @@ -324,5 +332,5 @@ func fetchSpecificRole(client *HTTPClient, role string) (res []RoleData, err err func init() { // Add the --output flag with default value "text" - ListRoleCmd.Flags().String("output", "text", "Output format: 'text' or 'json'") + ListRoleCmd.Flags().StringP("output", "o", "text", "Output format: 'text' or 'json'") } diff --git a/cmd/stream.go b/cmd/stream.go index 12dbdb8..7dcef13 100644 --- a/cmd/stream.go +++ b/cmd/stream.go @@ -20,7 +20,7 @@ import ( "errors" "fmt" "io" - "net/http" + internalHTTP "pb/pkg/http" "strconv" "strings" "time" @@ -110,24 +110,39 @@ var AddStreamCmd = &cobra.Command{ Example: " pb stream add backend_logs", Short: "Create a new stream", Args: cobra.ExactArgs(1), - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + // Capture start time + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] - client := DefaultClient() + client := internalHTTP.DefaultClient(&DefaultProfile) req, err := client.NewRequest("PUT", "logstream/"+name, nil) if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } + // Capture execution time + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if resp.StatusCode == 200 { fmt.Printf("Created stream %s\n", StyleBold.Render(name)) } else { bytes, err := io.ReadAll(resp.Body) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } body := string(bytes) @@ -146,12 +161,21 @@ var StatStreamCmd = &cobra.Command{ Short: "Get statistics for a stream", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + // Capture start time + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] - client := DefaultClient() + client := internalHTTP.DefaultClient(&DefaultProfile) // Fetch stats data stats, err := fetchStats(&client, name) if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } @@ -163,12 +187,16 @@ var StatStreamCmd = &cobra.Command{ // Fetch retention data retention, err := fetchRetention(&client, name) if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } // Fetch alerts data alertsData, err := fetchAlerts(&client, name) if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } @@ -189,6 +217,8 @@ var StatStreamCmd = &cobra.Command{ jsonData, err := json.MarshalIndent(data, "", " ") if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } fmt.Println(string(jsonData)) @@ -245,7 +275,7 @@ var StatStreamCmd = &cobra.Command{ } func init() { - StatStreamCmd.Flags().String("output", "text", "Output format: text or json") + StatStreamCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format (text|json)") } var RemoveStreamCmd = &cobra.Command{ @@ -254,29 +284,43 @@ var RemoveStreamCmd = &cobra.Command{ Example: " pb stream remove backend_logs", Short: "Delete a stream", Args: cobra.ExactArgs(1), - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + // Capture start time + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] - client := DefaultClient() + client := internalHTTP.DefaultClient(&DefaultProfile) req, err := client.NewRequest("DELETE", "logstream/"+name, nil) if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } + // Capture execution time + cmd.Annotations["executionTime"] = time.Since(startTime).String() + if resp.StatusCode == 200 { - fmt.Printf("Removed stream %s\n", StyleBold.Render(name)) + fmt.Printf("Successfully deleted stream %s\n", StyleBold.Render(name)) } else { bytes, err := io.ReadAll(resp.Body) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } body := string(bytes) defer resp.Body.Close() - fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) } @@ -284,74 +328,53 @@ var RemoveStreamCmd = &cobra.Command{ }, } +// ListStreamCmd is the list command for streams var ListStreamCmd = &cobra.Command{ Use: "list", - Short: "List all streams", Example: " pb stream list", + Short: "List all streams", RunE: func(cmd *cobra.Command, _ []string) error { - client := DefaultClient() + // Capture start time + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + + client := internalHTTP.DefaultClient(&DefaultProfile) req, err := client.NewRequest("GET", "logstream", nil) if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { + // Capture error + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - // Read response body for error message + var streams []StreamListItem + if resp.StatusCode == 200 { bytes, err := io.ReadAll(resp.Body) if err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) return err } - body := string(bytes) - fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) - return nil - } - - var items []map[string]string - err = json.NewDecoder(resp.Body).Decode(&items) - if err != nil { - return err - } - - // Get output flag value - outputFormat, err := cmd.Flags().GetString("output") - if err != nil { - return err - } - - // Handle JSON output format - if outputFormat == "json" { - // Collect stream names for JSON output - streams := make([]string, len(items)) - for i, item := range items { - streams[i] = item["name"] - } - jsonOutput, err := json.MarshalIndent(streams, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal JSON output: %w", err) + if err := json.Unmarshal(bytes, &streams); err != nil { + cmd.Annotations["errors"] = fmt.Sprintf("Error: %s", err.Error()) + return err } - fmt.Println(string(jsonOutput)) - return nil - } - // Default to text output - if len(items) == 0 { - fmt.Println("No streams found") - return nil + for _, stream := range streams { + fmt.Println(stream.Render()) + } + } else { + fmt.Printf("Failed to fetch streams. Status Code: %s\n", resp.Status) } - fmt.Println() - for _, item := range items { - streamItem := StreamListItem{Name: item["name"]} - fmt.Print("• ") - fmt.Println(streamItem.Render()) - } - fmt.Println() return nil }, } @@ -361,13 +384,13 @@ func init() { ListStreamCmd.Flags().StringP("output", "o", "text", "Output format: 'text' or 'json'") } -func fetchStats(client *HTTPClient, name string) (data StreamStatsData, err error) { +func fetchStats(client *internalHTTP.HTTPClient, name string) (data StreamStatsData, err error) { req, err := client.NewRequest("GET", fmt.Sprintf("logstream/%s/stats", name), nil) if err != nil { return } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { return } @@ -388,13 +411,13 @@ func fetchStats(client *HTTPClient, name string) (data StreamStatsData, err erro return } -func fetchRetention(client *HTTPClient, name string) (data StreamRetentionData, err error) { +func fetchRetention(client *internalHTTP.HTTPClient, name string) (data StreamRetentionData, err error) { req, err := client.NewRequest("GET", fmt.Sprintf("logstream/%s/retention", name), nil) if err != nil { return } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { return } @@ -415,13 +438,13 @@ func fetchRetention(client *HTTPClient, name string) (data StreamRetentionData, return } -func fetchAlerts(client *HTTPClient, name string) (data AlertConfig, err error) { +func fetchAlerts(client *internalHTTP.HTTPClient, name string) (data AlertConfig, err error) { req, err := client.NewRequest("GET", fmt.Sprintf("logstream/%s/alert", name), nil) if err != nil { return } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { return } diff --git a/cmd/tail.go b/cmd/tail.go index c039c03..6ccde73 100644 --- a/cmd/tail.go +++ b/cmd/tail.go @@ -22,7 +22,9 @@ import ( "encoding/base64" "encoding/json" "fmt" + "pb/pkg/analytics" "pb/pkg/config" + internalHTTP "pb/pkg/http" "github.com/apache/arrow/go/v13/arrow/array" "github.com/apache/arrow/go/v13/arrow/flight" @@ -53,8 +55,8 @@ func tail(profile config.Profile, stream string) error { }) // get grpc url for this request - httpClient := DefaultClient() - about, err := FetchAbout(&httpClient) + httpClient := internalHTTP.DefaultClient(&DefaultProfile) + about, err := analytics.FetchAbout(&httpClient) if err != nil { return err } diff --git a/cmd/user.go b/cmd/user.go index 42aefe9..fc02679 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -20,8 +20,10 @@ import ( "encoding/json" "fmt" "io" + internalHTTP "pb/pkg/http" "strings" "sync" + "time" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" @@ -46,11 +48,18 @@ var addUser = &cobra.Command{ Short: "Add a new user", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) // Initialize Annotations map + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] - client := DefaultClient() + client := internalHTTP.DefaultClient(&DefaultProfile) users, err := fetchUsers(&client) if err != nil { + cmd.Annotations["error"] = err.Error() return err } @@ -58,6 +67,7 @@ var addUser = &cobra.Command{ return user.ID == name }) { fmt.Println("user already exists") + cmd.Annotations["error"] = "user already exists" return nil } @@ -68,6 +78,7 @@ var addUser = &cobra.Command{ // fetch the role names on the server var rolesOnServer []string if err := fetchRoles(&client, &rolesOnServer); err != nil { + cmd.Annotations["error"] = err.Error() return err } rolesOnServerArr := strings.Join(rolesOnServer, " ") @@ -76,7 +87,8 @@ var addUser = &cobra.Command{ for idx, role := range rolesToSetArr { rolesToSetArr[idx] = strings.TrimSpace(role) if !strings.Contains(rolesOnServerArr, rolesToSetArr[idx]) { - fmt.Printf("role %s doesn't exist, please create a role using `pb role add %s`\n", rolesToSetArr[idx], rolesToSetArr[idx]) + fmt.Printf("role %s doesn't exist, please create a role using pb role add %s\n", rolesToSetArr[idx], rolesToSetArr[idx]) + cmd.Annotations["error"] = fmt.Sprintf("role %s doesn't exist", rolesToSetArr[idx]) return nil } } @@ -86,16 +98,19 @@ var addUser = &cobra.Command{ putBody = bytes.NewBuffer([]byte(putBodyJSON)) req, err := client.NewRequest("POST", "user/"+name, putBody) if err != nil { + cmd.Annotations["error"] = err.Error() return err } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { + cmd.Annotations["error"] = err.Error() return err } bytes, err := io.ReadAll(resp.Body) if err != nil { + cmd.Annotations["error"] = err.Error() return err } body := string(bytes) @@ -103,8 +118,10 @@ var addUser = &cobra.Command{ if resp.StatusCode == 200 { fmt.Printf("Added user: %s \nPassword is: %s\nRole(s) assigned: %s\n", name, body, rolesToSet) + cmd.Annotations["error"] = "none" } else { fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + cmd.Annotations["error"] = fmt.Sprintf("request failed with status code %s", resp.Status) } return nil @@ -122,30 +139,34 @@ var RemoveUserCmd = &cobra.Command{ Example: " pb user remove bob", Short: "Delete a user", Args: cobra.ExactArgs(1), - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + name := args[0] - client := DefaultClient() + client := internalHTTP.DefaultClient(&DefaultProfile) req, err := client.NewRequest("DELETE", "user/"+name, nil) if err != nil { + cmd.Annotations["error"] = err.Error() return err } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { + cmd.Annotations["error"] = err.Error() return err } if resp.StatusCode == 200 { fmt.Printf("Removed user %s\n", StyleBold.Render(name)) + cmd.Annotations["error"] = "none" } else { - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - body := string(bytes) - defer resp.Body.Close() - - fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + body, _ := io.ReadAll(resp.Body) + fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, string(body)) + cmd.Annotations["error"] = fmt.Sprintf("request failed with status code %s", resp.Status) } return nil @@ -162,12 +183,18 @@ var SetUserRoleCmd = &cobra.Command{ } return nil }, - RunE: func(_ *cobra.Command, args []string) error { - name := args[0] + RunE: func(cmd *cobra.Command, args []string) error { + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() - client := DefaultClient() + name := args[0] + client := internalHTTP.DefaultClient(&DefaultProfile) users, err := fetchUsers(&client) if err != nil { + cmd.Annotations["error"] = err.Error() return err } @@ -175,25 +202,24 @@ var SetUserRoleCmd = &cobra.Command{ return user.ID == name }) { fmt.Printf("user doesn't exist. Please create the user with `pb user add %s`\n", name) + cmd.Annotations["error"] = "user does not exist" return nil } - // fetch all the roles to be applied to this user rolesToSet := args[1] rolesToSetArr := strings.Split(rolesToSet, ",") - - // fetch the role names on the server var rolesOnServer []string if err := fetchRoles(&client, &rolesOnServer); err != nil { + cmd.Annotations["error"] = err.Error() return err } rolesOnServerArr := strings.Join(rolesOnServer, " ") - // validate if roles to be applied are actually present on the server for idx, role := range rolesToSetArr { rolesToSetArr[idx] = strings.TrimSpace(role) if !strings.Contains(rolesOnServerArr, rolesToSetArr[idx]) { fmt.Printf("role %s doesn't exist, please create a role using `pb role add %s`\n", rolesToSetArr[idx], rolesToSetArr[idx]) + cmd.Annotations["error"] = fmt.Sprintf("role %s doesn't exist", rolesToSetArr[idx]) return nil } } @@ -203,16 +229,19 @@ var SetUserRoleCmd = &cobra.Command{ putBody = bytes.NewBuffer([]byte(putBodyJSON)) req, err := client.NewRequest("PUT", "user/"+name+"/role", putBody) if err != nil { + cmd.Annotations["error"] = err.Error() return err } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { + cmd.Annotations["error"] = err.Error() return err } bytes, err := io.ReadAll(resp.Body) if err != nil { + cmd.Annotations["error"] = err.Error() return err } body := string(bytes) @@ -220,8 +249,10 @@ var SetUserRoleCmd = &cobra.Command{ if resp.StatusCode == 200 { fmt.Printf("Added role(s) %s to user %s\n", rolesToSet, name) + cmd.Annotations["error"] = "none" } else { fmt.Printf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + cmd.Annotations["error"] = fmt.Sprintf("request failed with status code %s", resp.Status) } return nil @@ -233,14 +264,21 @@ var ListUserCmd = &cobra.Command{ Short: "List all users", Example: " pb user list", RunE: func(cmd *cobra.Command, _ []string) error { - client := DefaultClient() + startTime := time.Now() + cmd.Annotations = make(map[string]string) + defer func() { + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + + client := internalHTTP.DefaultClient(&DefaultProfile) users, err := fetchUsers(&client) if err != nil { + cmd.Annotations["error"] = err.Error() return err } roleResponses := make([]struct { - data []string // Collects roles as strings for text output + data []string err error }, len(users)) @@ -254,7 +292,6 @@ var ListUserCmd = &cobra.Command{ var userRolesData UserRoleData userRolesData, out.err = fetchUserRoles(client, userID) if out.err == nil { - // Collect role names for this user for role := range userRolesData { out.data = append(out.data, role) } @@ -265,13 +302,12 @@ var ListUserCmd = &cobra.Command{ wsg.Wait() - // Get the output format, defaulting to empty (existing behavior) outputFormat, err := cmd.Flags().GetString("output") if err != nil { + cmd.Annotations["error"] = err.Error() return err } - // JSON output if specified if outputFormat == "json" { usersWithRoles := make([]map[string]interface{}, len(users)) for idx, user := range users { @@ -282,13 +318,14 @@ var ListUserCmd = &cobra.Command{ } jsonOutput, err := json.MarshalIndent(usersWithRoles, "", " ") if err != nil { + cmd.Annotations["error"] = err.Error() return fmt.Errorf("failed to marshal JSON output: %w", err) } fmt.Println(string(jsonOutput)) + cmd.Annotations["error"] = "none" return nil } - // Text output if specified if outputFormat == "text" { fmt.Println() for idx, user := range users { @@ -301,10 +338,10 @@ var ListUserCmd = &cobra.Command{ } } fmt.Println() + cmd.Annotations["error"] = "none" return nil } - // Default output (existing layout) fmt.Println() for idx, user := range users { roles := roleResponses[idx] @@ -320,17 +357,18 @@ var ListUserCmd = &cobra.Command{ } fmt.Println() + cmd.Annotations["error"] = "none" return nil }, } -func fetchUsers(client *HTTPClient) (res []UserData, err error) { +func fetchUsers(client *internalHTTP.HTTPClient) (res []UserData, err error) { req, err := client.NewRequest("GET", "user", nil) if err != nil { return } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { return } @@ -355,12 +393,12 @@ func fetchUsers(client *HTTPClient) (res []UserData, err error) { return } -func fetchUserRoles(client *HTTPClient, user string) (res UserRoleData, err error) { +func fetchUserRoles(client *internalHTTP.HTTPClient, user string) (res UserRoleData, err error) { req, err := client.NewRequest("GET", fmt.Sprintf("user/%s/role", user), nil) if err != nil { return } - resp, err := client.client.Do(req) + resp, err := client.Client.Do(req) if err != nil { return } diff --git a/cmd/version.go b/cmd/version.go index 9b7a266..21cdeb5 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -16,7 +16,11 @@ package cmd import ( + "encoding/json" "fmt" + "pb/pkg/analytics" + internalHTTP "pb/pkg/http" + "time" "github.com/spf13/cobra" ) @@ -27,25 +31,71 @@ var VersionCmd = &cobra.Command{ Short: "Print version", Long: "Print version and commit information", Example: " pb version", + Run: func(cmd *cobra.Command, _ []string) { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + + startTime := time.Now() + defer func() { + // Capture the execution time in annotations + cmd.Annotations["executionTime"] = time.Since(startTime).String() + }() + + err := PrintVersion("1.0.0", "abc123") // Replace with actual version and commit values + if err != nil { + cmd.Annotations["error"] = err.Error() + } + }, } -// PrintVersion prints version information -func PrintVersion(version, commit string) { - client := DefaultClient() +func init() { + VersionCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format (text|json)") +} - fmt.Printf("\n%s \n", StandardStyleAlt.Render("pb version")) - fmt.Printf("- %s %s\n", StandardStyleBold.Render("version: "), version) - fmt.Printf("- %s %s\n\n", StandardStyleBold.Render("commit: "), commit) +// PrintVersion prints version information +func PrintVersion(version, commit string) error { + client := internalHTTP.DefaultClient(&DefaultProfile) + // Fetch server information if err := PreRun(); err != nil { - return + return fmt.Errorf("error in PreRun: %w", err) } - about, err := FetchAbout(&client) + + about, err := analytics.FetchAbout(&client) if err != nil { - return + return fmt.Errorf("error fetching server information: %w", err) + } + + // Output as JSON if specified + if outputFormat == "json" { + versionInfo := map[string]interface{}{ + "client": map[string]string{ + "version": version, + "commit": commit, + }, + "server": map[string]string{ + "url": DefaultProfile.URL, + "version": about.Version, + "commit": about.Commit, + }, + } + jsonData, err := json.MarshalIndent(versionInfo, "", " ") + if err != nil { + return fmt.Errorf("error generating JSON output: %w", err) + } + fmt.Println(string(jsonData)) + return nil } + // Default: Output as text + fmt.Printf("\n%s \n", StandardStyleAlt.Render("pb version")) + fmt.Printf("- %s %s\n", StandardStyleBold.Render("version: "), version) + fmt.Printf("- %s %s\n\n", StandardStyleBold.Render("commit: "), commit) + fmt.Printf("%s %s \n", StandardStyleAlt.Render("Connected to"), StandardStyleBold.Render(DefaultProfile.URL)) fmt.Printf("- %s %s\n", StandardStyleBold.Render("version: "), about.Version) fmt.Printf("- %s %s\n\n", StandardStyleBold.Render("commit: "), about.Commit) + + return nil } diff --git a/go.mod b/go.mod index d71b784..8f39a8e 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,12 @@ require ( github.com/charmbracelet/bubbletea v0.26.6 github.com/charmbracelet/lipgloss v0.12.1 github.com/dustin/go-humanize v1.0.1 + github.com/oklog/ulid/v2 v2.1.0 + github.com/spf13/pflag v1.0.5 golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 golang.org/x/term v0.21.0 google.golang.org/grpc v1.64.1 + gopkg.in/yaml.v2 v2.4.0 ) require ( @@ -25,7 +28,6 @@ require ( github.com/klauspost/compress v1.15.15 // indirect github.com/klauspost/cpuid/v2 v2.2.3 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/zeebo/xxh3 v1.0.2 // indirect golang.org/x/mod v0.18.0 // indirect @@ -39,7 +41,7 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/evertras/bubble-table v0.15.2 - github.com/muesli/termenv v0.15.2 + github.com/muesli/termenv v0.15.2 // indirect github.com/pelletier/go-toml/v2 v2.0.9 github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect ) diff --git a/go.sum b/go.sum index 6857252..ea491fe 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,9 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= @@ -120,7 +123,10 @@ google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index c7b9e49..55985fe 100644 --- a/main.go +++ b/main.go @@ -20,12 +20,17 @@ import ( "errors" "fmt" "os" - "pb/cmd" + + pb "pb/cmd" + "pb/pkg/analytics" "pb/pkg/config" + "sync" "github.com/spf13/cobra" ) +var wg sync.WaitGroup + // populated at build time var ( Version string @@ -47,89 +52,152 @@ func defaultInitialProfile() config.Profile { // Root command var cli = &cobra.Command{ - Use: "pb", - Short: "\nParseable command line interface", - Long: "\npb is the command line interface for Parseable", + Use: "pb", + Short: "\nParseable command line interface", + Long: "\npb is the command line interface for Parseable", + PersistentPreRunE: analytics.CheckAndCreateULID, RunE: func(command *cobra.Command, _ []string) error { if p, _ := command.Flags().GetBool(versionFlag); p { - cmd.PrintVersion(Version, Commit) + pb.PrintVersion(Version, Commit) return nil } return errors.New("no command or flag supplied") }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if os.Getenv("PB_ANALYTICS") == "disable" { + return + } + wg.Add(1) + go func() { + defer wg.Done() + analytics.PostRunAnalytics(cmd, "cli", args) + }() + }, } var profile = &cobra.Command{ - Use: "profile", - Short: "Manage different Parseable targets", - Long: "\nuse profile command to configure different Parseable instances. Each profile takes a URL and credentials.", + Use: "profile", + Short: "Manage different Parseable targets", + Long: "\nuse profile command to configure different Parseable instances. Each profile takes a URL and credentials.", + PersistentPreRunE: combinedPreRun, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if os.Getenv("PB_ANALYTICS") == "disable" { + return + } + wg.Add(1) + go func() { + defer wg.Done() + analytics.PostRunAnalytics(cmd, "profile", args) + }() + }, } var user = &cobra.Command{ Use: "user", Short: "Manage users", Long: "\nuser command is used to manage users.", - PersistentPreRunE: cmd.PreRunDefaultProfile, + PersistentPreRunE: combinedPreRun, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if os.Getenv("PB_ANALYTICS") == "disable" { + return + } + wg.Add(1) + go func() { + defer wg.Done() + analytics.PostRunAnalytics(cmd, "user", args) + }() + }, } var role = &cobra.Command{ Use: "role", Short: "Manage roles", Long: "\nrole command is used to manage roles.", - PersistentPreRunE: cmd.PreRunDefaultProfile, + PersistentPreRunE: combinedPreRun, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if os.Getenv("PB_ANALYTICS") == "disable" { + return + } + wg.Add(1) + go func() { + defer wg.Done() + analytics.PostRunAnalytics(cmd, "role", args) + }() + }, } var stream = &cobra.Command{ Use: "stream", Short: "Manage streams", Long: "\nstream command is used to manage streams.", - PersistentPreRunE: cmd.PreRunDefaultProfile, + PersistentPreRunE: combinedPreRun, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if os.Getenv("PB_ANALYTICS") == "disable" { + return + } + wg.Add(1) + go func() { + defer wg.Done() + analytics.PostRunAnalytics(cmd, "stream", args) + }() + }, } var query = &cobra.Command{ Use: "query", Short: "Run SQL query on a log stream", Long: "\nRun SQL query on a log stream. Default output format is json. Use -i flag to open interactive table view.", - PersistentPreRunE: cmd.PreRunDefaultProfile, + PersistentPreRunE: combinedPreRun, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if os.Getenv("PB_ANALYTICS") == "disable" { + return + } + wg.Add(1) + go func() { + defer wg.Done() + analytics.PostRunAnalytics(cmd, "query", args) + }() + }, } func main() { - profile.AddCommand(cmd.AddProfileCmd) - profile.AddCommand(cmd.RemoveProfileCmd) - profile.AddCommand(cmd.ListProfileCmd) - profile.AddCommand(cmd.DefaultProfileCmd) + profile.AddCommand(pb.AddProfileCmd) + profile.AddCommand(pb.RemoveProfileCmd) + profile.AddCommand(pb.ListProfileCmd) + profile.AddCommand(pb.DefaultProfileCmd) - user.AddCommand(cmd.AddUserCmd) - user.AddCommand(cmd.RemoveUserCmd) - user.AddCommand(cmd.ListUserCmd) - user.AddCommand(cmd.SetUserRoleCmd) + user.AddCommand(pb.AddUserCmd) + user.AddCommand(pb.RemoveUserCmd) + user.AddCommand(pb.ListUserCmd) + user.AddCommand(pb.SetUserRoleCmd) - role.AddCommand(cmd.AddRoleCmd) - role.AddCommand(cmd.RemoveRoleCmd) - role.AddCommand(cmd.ListRoleCmd) + role.AddCommand(pb.AddRoleCmd) + role.AddCommand(pb.RemoveRoleCmd) + role.AddCommand(pb.ListRoleCmd) - stream.AddCommand(cmd.AddStreamCmd) - stream.AddCommand(cmd.RemoveStreamCmd) - stream.AddCommand(cmd.ListStreamCmd) - stream.AddCommand(cmd.StatStreamCmd) + stream.AddCommand(pb.AddStreamCmd) + stream.AddCommand(pb.RemoveStreamCmd) + stream.AddCommand(pb.ListStreamCmd) + stream.AddCommand(pb.StatStreamCmd) - query.AddCommand(cmd.QueryCmd) - query.AddCommand(cmd.SavedQueryList) + query.AddCommand(pb.QueryCmd) + query.AddCommand(pb.SavedQueryList) cli.AddCommand(profile) cli.AddCommand(query) cli.AddCommand(stream) cli.AddCommand(user) cli.AddCommand(role) - cli.AddCommand(cmd.TailCmd) + cli.AddCommand(pb.TailCmd) - cli.AddCommand(cmd.AutocompleteCmd) + cli.AddCommand(pb.AutocompleteCmd) // Set as command - cmd.VersionCmd.Run = func(_ *cobra.Command, _ []string) { - cmd.PrintVersion(Version, Commit) + pb.VersionCmd.Run = func(_ *cobra.Command, _ []string) { + pb.PrintVersion(Version, Commit) } - cli.AddCommand(cmd.VersionCmd) + + cli.AddCommand(pb.VersionCmd) // set as flag cli.Flags().BoolP(versionFlag, versionFlagShort, false, "Print version") @@ -173,4 +241,19 @@ func main() { if err != nil { os.Exit(1) } + wg.Wait() +} + +// Wrapper to combine existing pre-run logic and ULID check +func combinedPreRun(cmd *cobra.Command, args []string) error { + err := pb.PreRunDefaultProfile(cmd, args) + if err != nil { + return fmt.Errorf("error initializing default profile: %w", err) + } + + if err := analytics.CheckAndCreateULID(cmd, args); err != nil { + return fmt.Errorf("error while creating ulid: %v", err) + } + + return nil } diff --git a/pkg/analytics/analytics.go b/pkg/analytics/analytics.go new file mode 100644 index 0000000..816a1b0 --- /dev/null +++ b/pkg/analytics/analytics.go @@ -0,0 +1,387 @@ +package analytics + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "math/rand" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "pb/pkg/config" + internalHTTP "pb/pkg/http" + + "github.com/oklog/ulid/v2" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "gopkg.in/yaml.v2" +) + +type Event struct { + CLIVersion string `json:"cli_version"` + ULID string `json:"ulid"` + CommitHash string `json:"commit_hash"` + OSName string `json:"os_name"` + OSVersion string `json:"os_version"` + ReportCreatedAt string `json:"report_created_at"` + Command Command `json:"command"` + Errors *string `json:"errors"` + ExecutionTimestamp string `json:"execution_timestamp"` +} + +// About struct +type About struct { + Version string `json:"version"` + UIVersion string `json:"uiVersion"` + Commit string `json:"commit"` + DeploymentID string `json:"deploymentId"` + UpdateAvailable bool `json:"updateAvailable"` + LatestVersion string `json:"latestVersion"` + LLMActive bool `json:"llmActive"` + LLMProvider string `json:"llmProvider"` + OIDCActive bool `json:"oidcActive"` + License string `json:"license"` + Mode string `json:"mode"` + Staging string `json:"staging"` + HotTier string `json:"hotTier"` + GRPCPort int `json:"grpcPort"` + Store Store `json:"store"` + Analytics Analytics `json:"analytics"` + QueryEngine string `json:"queryEngine"` +} + +// Store struct +type Store struct { + Type string `json:"type"` + Path string `json:"path"` +} + +// Analytics struct +type Analytics struct { + ClarityTag string `json:"clarityTag"` +} + +type Command struct { + Name string `json:"name"` + Arguments []string `json:"arguments"` + Flags map[string]string `json:"flags"` +} + +// Config struct for parsing YAML +type Config struct { + ULID string `yaml:"ulid"` +} + +// CheckAndCreateULID checks for a ULID in the config file and creates it if absent. +func CheckAndCreateULID(_ *cobra.Command, _ []string) error { + homeDir, err := os.UserHomeDir() + if err != nil { + fmt.Printf("could not find home directory: %v\n", err) + return err + } + + configPath := filepath.Join(homeDir, ".parseable", "config.yaml") + + // Check if config path exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + // Create the directory if needed + if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { + fmt.Printf("could not create config directory: %v\n", err) + return err + } + } + + // Read the config file + var config Config + data, err := os.ReadFile(configPath) + if err == nil { + // If the file exists, unmarshal the content + if err := yaml.Unmarshal(data, &config); err != nil { + fmt.Printf("could not parse config file: %v\n", err) + return err + } + } + + // Check if ULID is missing + if config.ULID == "" { + // Generate a new ULID + entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0) + ulidInstance := ulid.MustNew(ulid.Timestamp(time.Now()), entropy) + config.ULID = ulidInstance.String() + + newData, err := yaml.Marshal(&config) + if err != nil { + fmt.Printf("could not marshal config data: %v\n", err) + return err + } + + // Write updated config with ULID back to the file + if err := os.WriteFile(configPath, newData, 0644); err != nil { + fmt.Printf("could not write to config file: %v\n", err) + return err + } + fmt.Printf("Generated and saved new ULID: %s\n", config.ULID) + } + + return nil +} + +func PostRunAnalytics(cmd *cobra.Command, name string, args []string) { + executionTime := cmd.Annotations["executionTime"] + commandError := cmd.Annotations["error"] + flags := make(map[string]string) + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + flags[flag.Name] = flag.Value.String() + }) + + // Call SendEvent in PostRunE + err := sendEvent( + name, + append(args, cmd.Name()), + &commandError, // Pass the error here if there was one + executionTime, + flags, + ) + if err != nil { + fmt.Println("Error sending analytics event:", err) + } + +} + +// sendEvent is a placeholder function to simulate sending an event after command execution. +func sendEvent(commandName string, arguments []string, errors *string, executionTimestamp string, flags map[string]string) error { + ulid, err := ReadUULD() + if err != nil { + return fmt.Errorf("could not load ULID: %v", err) + } + + profile, err := GetProfile() + if err != nil { + return fmt.Errorf("failed to get profile: %v", err) + } + + httpClient := internalHTTP.DefaultClient(&profile) + + about, err := FetchAbout(&httpClient) + if err != nil { + return fmt.Errorf("failed to get about metadata for profile: %v", err) + } + + // Create the Command struct + cmd := Command{ + Name: commandName, + Arguments: arguments, + Flags: flags, + } + + // Populate the Event struct with OS details and timestamp + event := Event{ + CLIVersion: about.Commit, + ULID: ulid, + CommitHash: about.Commit, + OSName: GetOSName(), + OSVersion: GetOSVersion(), + ReportCreatedAt: GetCurrentTimestamp(), + Command: cmd, + Errors: errors, + ExecutionTimestamp: executionTimestamp, + } + + // Marshal the event to JSON for sending + eventJSON, err := json.Marshal(event) + if err != nil { + return fmt.Errorf("failed to marshal event JSON: %v", err) + } + + // Define the target URL for the HTTP request + url := "https://analytics.parseable.io:80/pb" + + // Create the HTTP POST request + req, err := http.NewRequest("POST", url, bytes.NewBuffer(eventJSON)) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-P-Stream", "pb-usage") + + // Execute the HTTP request + resp, err := httpClient.Client.Do(req) + if err != nil { + return fmt.Errorf("failed to send event: %v", err) + } + defer resp.Body.Close() + + // Check for a non-2xx status code + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("received non-2xx response: %v", resp.Status) + } + + //fmt.Println("Event sent successfully:", string(eventJSON)) + return nil +} + +// GetOSName retrieves the OS name. +func GetOSName() string { + switch runtime.GOOS { + case "windows": + return "Windows" + case "darwin": + return "macOS" + case "linux": + return getLinuxDistro() + default: + return "Unknown" + } +} + +// GetOSVersion retrieves the OS version. +func GetOSVersion() string { + switch runtime.GOOS { + case "windows": + return getWindowsVersion() + case "darwin": + return getMacOSVersion() + case "linux": + return getLinuxVersion() + default: + return "Unknown" + } +} + +// GetCurrentTimestamp returns the current timestamp in ISO 8601 format. +func GetCurrentTimestamp() string { + return time.Now().Format(time.RFC3339) +} + +// GetFormattedTimestamp formats a given time.Time in ISO 8601 format. +func GetFormattedTimestamp(t time.Time) string { + return t.Format(time.RFC3339) +} + +// getLinuxDistro retrieves the Linux distribution name. +func getLinuxDistro() string { + data, err := os.ReadFile("/etc/os-release") + if err != nil { + return "Linux" + } + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "NAME=") { + return strings.Trim(line[5:], "\"") + } + } + return "Linux" +} + +// getLinuxVersion retrieves the Linux distribution version. +func getLinuxVersion() string { + data, err := os.ReadFile("/etc/os-release") + if err != nil { + return "Unknown" + } + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "VERSION_ID=") { + return strings.Trim(line[11:], "\"") + } + } + return "Unknown" +} + +// getMacOSVersion retrieves the macOS version. +func getMacOSVersion() string { + out, err := exec.Command("sw_vers", "-productVersion").Output() + if err != nil { + return "Unknown" + } + return strings.TrimSpace(string(out)) +} + +// getWindowsVersion retrieves the Windows version. +func getWindowsVersion() string { + out, err := exec.Command("cmd", "ver").Output() + if err != nil { + return "Unknown" + } + return strings.TrimSpace(string(out)) +} + +func ReadUULD() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("could not find home directory: %v", err) + } + + configPath := filepath.Join(homeDir, ".parseable", "config.yaml") + + // Check if config path exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return "", fmt.Errorf("config file does not exist, please run CheckAndCreateULID first") + } + + // Read the config file + var config Config + data, err := os.ReadFile(configPath) + if err != nil { + return "", fmt.Errorf("could not read config file: %v", err) + } + + // Unmarshal the content to get the ULID + if err := yaml.Unmarshal(data, &config); err != nil { + return "", fmt.Errorf("could not parse config file: %v", err) + } + + if config.ULID == "" { + return "", fmt.Errorf("ULID is missing in config file") + } + + return config.ULID, nil +} + +func FetchAbout(client *internalHTTP.HTTPClient) (about About, err error) { + req, err := client.NewRequest("GET", "about", nil) + if err != nil { + return + } + + resp, err := client.Client.Do(req) + if err != nil { + return + } + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + err = json.Unmarshal(bytes, &about) + } else { + body := string(bytes) + body = fmt.Sprintf("Request Failed\nStatus Code: %s\nResponse: %s\n", resp.Status, body) + err = errors.New(body) + } + return +} + +func GetProfile() (config.Profile, error) { + conf, err := config.ReadConfigFromFile() + if os.IsNotExist(err) { + return config.Profile{}, errors.New("no config found to run this command. add a profile using pb profile command") + } else if err != nil { + return config.Profile{}, err + } + + if conf.Profiles == nil || conf.DefaultProfile == "" { + return config.Profile{}, errors.New("no profile is configured to run this command. please create one using profile command") + } + + return conf.Profiles[conf.DefaultProfile], nil + +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 434113d..c57a9cc 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -17,6 +17,7 @@ package config import ( + "errors" "fmt" "net" "net/url" @@ -48,9 +49,9 @@ type Config struct { // Profile is the struct that holds the profile configuration type Profile struct { - URL string - Username string - Password string + URL string `json:"url"` + Username string `json:"username"` + Password string `json:"password,omitempty"` } func (p *Profile) GrpcAddr(port string) string { @@ -105,3 +106,19 @@ func ReadConfigFromFile() (config *Config, err error) { return config, nil } + +func GetProfile() (Profile, error) { + conf, err := ReadConfigFromFile() + if os.IsNotExist(err) { + return Profile{}, errors.New("no config found to run this command. add a profile using pb profile command") + } else if err != nil { + return Profile{}, err + } + + if conf.Profiles == nil || conf.DefaultProfile == "" { + return Profile{}, errors.New("no profile is configured to run this command. please create one using profile command") + } + + return conf.Profiles[conf.DefaultProfile], nil + +} diff --git a/cmd/client.go b/pkg/http/http.go similarity index 82% rename from cmd/client.go rename to pkg/http/http.go index 2675796..340d1b1 100644 --- a/cmd/client.go +++ b/pkg/http/http.go @@ -25,21 +25,21 @@ import ( ) type HTTPClient struct { - client http.Client - profile *config.Profile + Client http.Client + Profile *config.Profile } -func DefaultClient() HTTPClient { +func DefaultClient(profile *config.Profile) HTTPClient { return HTTPClient{ - client: http.Client{ + Client: http.Client{ Timeout: 60 * time.Second, }, - profile: &DefaultProfile, + Profile: profile, } } func (client *HTTPClient) baseAPIURL(path string) (x string) { - x, _ = url.JoinPath(client.profile.URL, "api/v1/", path) + x, _ = url.JoinPath(client.Profile.URL, "api/v1/", path) return } @@ -48,7 +48,7 @@ func (client *HTTPClient) NewRequest(method string, path string, body io.Reader) if err != nil { return } - req.SetBasicAuth(client.profile.Username, client.profile.Password) + req.SetBasicAuth(client.Profile.Username, client.Profile.Password) req.Header.Add("Content-Type", "application/json") return }