Skip to content

Commit

Permalink
Update the query command to have the -i flag
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 committed Oct 22, 2023
1 parent 39054ff commit 87805f5
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 94 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
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
}
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
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
25 changes: 13 additions & 12 deletions pkg/model/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ import (
"math"
"net/http"
"os"
"pb/pkg/config"
"pb/pkg/iterator"
"regexp"
"strings"
"sync"
"time"

"pb/pkg/config"
"pb/pkg/iterator"

"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
Expand All @@ -49,8 +50,8 @@ const (

// Style for this widget
var (
FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"}
FocusSecondry = lipgloss.AdaptiveColor{Light: "18", Dark: "220"}
FocusPrimary = lipgloss.AdaptiveColor{Light: "16", Dark: "226"}
FocusSecondary = lipgloss.AdaptiveColor{Light: "18", Dark: "220"}

StandardPrimary = lipgloss.AdaptiveColor{Light: "235", Dark: "255"}
StandardSecondary = lipgloss.AdaptiveColor{Light: "238", Dark: "254"}
Expand All @@ -67,7 +68,7 @@ var (

baseStyle = lipgloss.NewStyle().BorderForeground(StandardPrimary)
baseBoldUnderlinedStyle = lipgloss.NewStyle().BorderForeground(StandardPrimary).Bold(true)
headerStyle = lipgloss.NewStyle().Inherit(baseStyle).Foreground(FocusSecondry).Bold(true)
headerStyle = lipgloss.NewStyle().Inherit(baseStyle).Foreground(FocusSecondary).Bold(true)
tableStyle = lipgloss.NewStyle().Inherit(baseStyle).Align(lipgloss.Left)
)

Expand Down Expand Up @@ -96,7 +97,7 @@ var (
key.NewBinding(key.WithKeys("ctrl+r"), key.WithHelp("ctrl r", "(re) run query")),
}

pagiatorKeyBinds = []key.Binding{
paginatorKeyBinds = []key.Binding{
key.NewBinding(key.WithKeys("ctrl+r"), key.WithHelp("ctrl r", "Fetch Next Minute")),
key.NewBinding(key.WithKeys("ctrl+b"), key.WithHelp("ctrl b", "Fetch Prev Minute")),
}
Expand Down Expand Up @@ -196,10 +197,10 @@ func createIteratorFromModel(m *QueryModel) *iterator.QueryIterator[QueryData, F
return &iter
}

func NewQueryModel(profile config.Profile, stream string, duration uint) QueryModel {
func NewQueryModel(profile config.Profile, queryStr string, startTime, endTime time.Time) QueryModel {
w, h, _ := term.GetSize(int(os.Stdout.Fd()))

inputs := NewTimeInputModel(duration)
inputs := NewTimeInputModel(startTime, endTime)

columns := []table.Column{
table.NewColumn("Id", "Id", 5),
Expand Down Expand Up @@ -228,12 +229,12 @@ func NewQueryModel(profile config.Profile, stream string, duration uint) QueryMo
query.SetHeight(2)
query.SetWidth(70)
query.ShowLineNumbers = true
query.SetValue(fmt.Sprintf("select * from %s", stream))
query.SetValue(queryStr)
query.KeyMap = textAreaKeyMap
query.Focus()

help := help.New()
help.Styles.FullDesc = lipgloss.NewStyle().Foreground(FocusSecondry)
help.Styles.FullDesc = lipgloss.NewStyle().Foreground(FocusSecondary)

model := QueryModel{
width: w,
Expand All @@ -245,7 +246,7 @@ func NewQueryModel(profile config.Profile, stream string, duration uint) QueryMo
profile: profile,
help: help,
queryIterator: nil,
status: NewStatusBar(profile.URL, stream, w),
status: NewStatusBar(profile.URL, w),
}
model.queryIterator = createIteratorFromModel(&model)
return model
Expand Down Expand Up @@ -442,7 +443,7 @@ func (m QueryModel) View() string {
}

if m.queryIterator != nil {
helpKeys = append(helpKeys, pagiatorKeyBinds)
helpKeys = append(helpKeys, paginatorKeyBinds)
} else {
helpKeys = append(helpKeys, additionalKeyBinds)
}
Expand Down
Loading

0 comments on commit 87805f5

Please sign in to comment.