Skip to content

Commit

Permalink
Update the query command to have the -i flag (#31)
Browse files Browse the repository at this point in the history
Also clean up the query command so it works without
any inputs provided. In such cases it uses sensible
defaults.
  • Loading branch information
nitisht authored Oct 22, 2023
1 parent 39054ff commit 3adb3fc
Show file tree
Hide file tree
Showing 16 changed files with 167 additions and 104 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
## pb

pb is the command line interface for [Parseable Server](https://github.com/parseablehq/parseable). pb allows you to manage Streams, Users, and Data on Parseable Server. You can use pb to manage multiple Parseable Server instances using Profiles.
Dashboard fatigue is one of key reasons for poor adoption of logging tools among developers. With pb, we intend to bring the familiar command line interface for querying and analyzing log data at scale.

We believe dashboard fatigue is one of key reasons for poor adoption of logging tools among developers. With pb, we intend to bring the familiar command line interface for querying and analyzing log data at scale.
pb is the command line interface for [Parseable Server](https://github.com/parseablehq/parseable). pb allows you to manage Streams, Users, and Data on Parseable Server. You can use pb to manage multiple Parseable Server instances using Profiles.

![pb banner](https://github.com/parseablehq/.github/blob/main/images/pb/pb.png?raw=true)

Expand All @@ -22,7 +22,7 @@ chmod +x pb && mv pb /usr/local/bin
pb comes configured with `demo` profile as the default. This means you can directly start using pb against the [demo Parseable Server](https://demo.parseable.io). For example, to query the stream `backend` on demo server, run:

```bash
pb query backend
pb query backend
```

#### Profiles
Expand Down
3 changes: 2 additions & 1 deletion cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import (
"io"
"net/http"
"net/url"
"pb/pkg/config"
"time"

"pb/pkg/config"
)

type HTTPClient struct {
Expand Down
1 change: 1 addition & 0 deletions cmd/pre.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cmd
import (
"errors"
"os"

"pb/pkg/config"

"github.com/spf13/cobra"
Expand Down
1 change: 1 addition & 0 deletions cmd/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"net/url"
"os"

"pb/pkg/config"
"pb/pkg/model/credential"
"pb/pkg/model/defaultprofile"
Expand Down
160 changes: 112 additions & 48 deletions cmd/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,93 +17,99 @@ package cmd

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"time"

"pb/pkg/model"
"strconv"

tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
)

var (
durationFlag = "duration"
durationFlagShort = "d"
defaultDuration = "10"

startFlag = "from"
startFlagShort = "f"
defaultStart = "1m"

endFlag = "to"
endFlagShort = "t"
defaultEnd = "now"

interactiveFlag = "interactive"
interactiveFlagShort = "i"
)

var queryInteractive = &cobra.Command{
Use: "i [stream-name] --duration 10",
Example: " pb query frontend --duration 10",
Short: "Interactive query table view",
Long: "\n command is used to open a prompt to query a stream.",
Args: cobra.ExactArgs(1),
var query = &cobra.Command{
Use: "query [query] [flags]",
Example: " pb query \"select * from frontend\" --from=10m --to=now",
Short: "Run SQL query on a log stream",
Long: "\nqRun SQL query on a log stream. Default output format is json. Use -i flag to open interactive table view.",
Args: cobra.MaximumNArgs(1),
PreRunE: PreRunDefaultProfile,
RunE: func(command *cobra.Command, args []string) error {
stream := args[0]
duration, _ := command.Flags().GetString(durationFlag)

if duration == "" {
duration = defaultDuration
var query string

// if no query is provided set it to default "select * from <steam-name>"
// <steam-name> here is the first stream that server returns
if len(args) == 0 || args[0] == "" || args[0] == " " {
stream, err := fetchFirstStream()
if err != nil {
return err
}
query = fmt.Sprintf("select * from %s", stream)
} else {
query = args[0]
}
durationInt, err := strconv.Atoi(duration)

start, err := command.Flags().GetString(startFlag)
if err != nil {
return err
}

p := tea.NewProgram(model.NewQueryModel(DefaultProfile, stream, uint(durationInt)), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Printf("there's been an error: %v", err)
os.Exit(1)
if start == "" {
start = defaultStart
}

return nil
},
}

var queryJSON = &cobra.Command{
Use: "query [query] --from=10m --to=now",
Example: " pb query \"select * from frontend\" --from=10m --to=now",
Short: "Run SQL query",
Long: "\nquery command is used to run query. Output format is json string",
Args: cobra.ExactArgs(1),
PreRunE: PreRunDefaultProfile,
RunE: func(command *cobra.Command, args []string) error {
query := args[0]
start, _ := command.Flags().GetString(startFlag)
end, _ := command.Flags().GetString(endFlag)

if start == "" {
start = defaultStart
if err != nil {
return err
}
if end == "" {
end = defaultEnd
}

interactive, _ := command.Flags().GetBool(interactiveFlag)
if err != nil {
return err
}

startTime, endTime, err := parseTime(start, end)
if err != nil {
return err
}

if interactive {
p := tea.NewProgram(model.NewQueryModel(DefaultProfile, query, startTime, endTime), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Printf("there's been an error: %v", err)
os.Exit(1)
}
return nil
}

client := DefaultClient()
return fetchData(&client, query, start, end)
},
}

var QueryInteractiveCmd = func() *cobra.Command {
queryInteractive.Flags().StringP(durationFlag, durationFlagShort, defaultDuration, "specify the duration in minutes for which queries should be executed. Defaults to 10 minutes")
return queryInteractive
}()

var QueryCmd = func() *cobra.Command {
queryJSON.Flags().StringP(startFlag, startFlagShort, defaultStart, "Specify start datetime of query. Supports RFC3999 time format and durations (ex. 10m, 1hr ..) ")
queryJSON.Flags().StringP(endFlag, endFlagShort, defaultEnd, "Specify end datetime of query. Supports RFC3999 time format and literal - now ")
queryJSON.AddCommand(queryInteractive)
return queryJSON
query.Flags().BoolP(interactiveFlag, interactiveFlagShort, false, "open the query result in interactive mode")
query.Flags().StringP(startFlag, startFlagShort, defaultStart, "Start time for query. Takes date as '2023-10-12T07:20:50.52Z' or string like '10m', '1hr'")
query.Flags().StringP(endFlag, endFlagShort, defaultEnd, "End time for query. Takes date as '2023-10-12T07:20:50.52Z' or 'now'")
return query
}()

func fetchData(client *HTTPClient, query string, startTime string, endTime string) (err error) {
Expand Down Expand Up @@ -134,3 +140,61 @@ func fetchData(client *HTTPClient, query string, startTime string, endTime strin
}
return
}

func fetchFirstStream() (string, error) {
client := DefaultClient()
req, err := client.NewRequest("GET", "logstream", nil)
if err != nil {
return "", err
}

resp, err := client.client.Do(req)
if err != nil {
return "", err
}

if resp.StatusCode == 200 {
items := []map[string]string{}
if err := json.NewDecoder(resp.Body).Decode(&items); err != nil {
return "", err
}
defer resp.Body.Close()

if len(items) == 0 {
return "", errors.New("no stream found on the server, please create a stream to proceed")
}
// return with the first stream that is present in the list
for _, v := range items {
return v["name"], nil
}
}
return "", fmt.Errorf("received error status code %d from server", resp.StatusCode)
}

// Returns start and end time for query in RFC3339 format
func parseTime(start, end string) (time.Time, time.Time, error) {
if start == defaultStart && end == defaultEnd {
return time.Now().Add(-1 * time.Minute), time.Now(), nil
}

startTime, err := time.Parse(time.RFC3339, start)
if err != nil {
// try parsing as duration
duration, err := time.ParseDuration(start)
if err != nil {
return time.Time{}, time.Time{}, err
}
startTime = time.Now().Add(-1 * duration)
}

endTime, err := time.Parse(time.RFC3339, end)
if err != nil {
if end == "now" {
endTime = time.Now()
} else {
return time.Time{}, time.Time{}, err
}
}

return startTime, endTime, nil
}
3 changes: 2 additions & 1 deletion cmd/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import (
"fmt"
"io"
"os"
"pb/pkg/model/role"
"strings"
"sync"

"pb/pkg/model/role"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
Expand Down
4 changes: 2 additions & 2 deletions cmd/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ var StatStreamCmd = &cobra.Command{
return err
}

isRententionSet := len(retention) > 0
isRetentionSet := len(retention) > 0

fmt.Println(styleBold.Render("\nInfo:"))
fmt.Printf(" Event Count: %d\n", ingestionCount)
Expand All @@ -161,7 +161,7 @@ var StatStreamCmd = &cobra.Command{
100-(float64(storageSize)/float64(ingestionSize))*100, "%")
fmt.Println()

if isRententionSet {
if isRetentionSet {
fmt.Println(styleBold.Render("Retention:"))
for _, item := range retention {
fmt.Printf(" Action: %s\n", styleBold.Render(item.Action))
Expand Down
1 change: 1 addition & 0 deletions cmd/tail.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"

"pb/pkg/config"

"github.com/apache/arrow/go/v13/arrow/array"
Expand Down
3 changes: 2 additions & 1 deletion cmd/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import (
"fmt"
"io"
"os"
"pb/pkg/model/role"
"sync"

"pb/pkg/model/role"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
Expand Down
5 changes: 3 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main
import (
"errors"
"os"

"pb/cmd"
"pb/pkg/config"

Expand Down Expand Up @@ -60,8 +61,8 @@ var cli = &cobra.Command{

var profile = &cobra.Command{
Use: "profile",
Short: "Manage profiles",
Long: "\nuse profile command to configure (multiple) Parseable instances. Each profile takes a URL and credentials.",
Short: "Manage different Parseable targets",
Long: "\nuse profile command to configure different Parseable instances. Each profile takes a URL and credentials.",
}

var user = &cobra.Command{
Expand Down
3 changes: 2 additions & 1 deletion pkg/model/credential/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
package credential

import (
"pb/pkg/model/button"
"strings"

"pb/pkg/model/button"

"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
Expand Down
1 change: 1 addition & 0 deletions pkg/model/defaultprofile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package defaultprofile
import (
"fmt"
"io"

"pb/pkg/config"

"github.com/charmbracelet/bubbles/list"
Expand Down
Loading

0 comments on commit 3adb3fc

Please sign in to comment.