Skip to content

feat(logs): add new command #180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions e2e/testscripts/logs/logs.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Get log entries
exec algolia logs list
! stderr .
stdout -count=5 url

# Wrong log type should return error
! exec algolia logs list --type foo
stderr 'invalid argument'
117 changes: 117 additions & 0 deletions pkg/cmd/logs/list/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package list

import (
"fmt"

"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"

"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/iostreams"
)

type LogOptions struct {
Config config.IConfig
IO *iostreams.IOStreams

SearchClient func() (*search.APIClient, error)

PrintFlags *cmdutil.PrintFlags

Entries int32
Start int32
LogType string
IndexName *string
}

// NewListCmd returns a new command for retrieving logs
func NewListCmd(f *cmdutil.Factory, runF func(*LogOptions) error) *cobra.Command {
opts := &LogOptions{
IO: f.IOStreams,
Config: f.Config,
SearchClient: f.SearchClient,
PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"),
}

cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List log entries",
RunE: func(cmd *cobra.Command, args []string) error {
if runF != nil {
return runF(opts)
}
return runLogsCmd(opts)
},
Annotations: map[string]string{
"acls": "logs",
},
Example: heredoc.Doc(`
# Show the latest 5 Search API log entries
$ algolia logs

# Show the log entries 11 to 20
$ algolia logs --entries 10 --start 11

# Only show log entries with errors
$ algolia logs --type error
`),
}

opts.PrintFlags.AddFlags(cmd)

cmd.Flags().Int32VarP(&opts.Entries, "entries", "e", 5, "How many log entries to show")
cmd.Flags().
Int32VarP(&opts.Start, "start", "s", 1, "Number of the first log entry to retrieve (starts with 1)")
cmdutil.StringEnumFlag(
cmd,
&opts.LogType,
"type",
"t",
"all",
[]string{"all", "build", "query", "error"},
"Type of log entries",
)

cmdutil.NilStringFlag(cmd, &opts.IndexName, "index", "i", "Filter logs by index name")

return cmd
}

func runLogsCmd(opts *LogOptions) error {
client, err := opts.SearchClient()
if err != nil {
return err
}

p, err := opts.PrintFlags.ToPrinter()
if err != nil {
return err
}

realLogType, err := search.NewLogTypeFromValue(opts.LogType)
if err != nil {
return fmt.Errorf("invalid log type %s: %v", opts.LogType, err)
}

request := client.NewApiGetLogsRequest().
// Offset is 0 based
WithOffset(opts.Start - 1).
WithLength(opts.Entries).
WithType(*realLogType)

if opts.IndexName != nil {
request = request.WithIndexName(*opts.IndexName)
}

opts.IO.StartProgressIndicatorWithLabel("Retrieving logs")
res, err := client.GetLogs(request)
opts.IO.StopProgressIndicator()
if err != nil {
return err
}

return p.Print(opts.IO, res)
}
66 changes: 66 additions & 0 deletions pkg/cmd/logs/list/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package list

import (
"testing"

"github.com/algolia/cli/pkg/cmdutil"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewLogsCmd(t *testing.T) {
testIndexName := "foo"
tests := []struct {
name string
cli string
wantsErr bool
wantsOpts LogOptions
}{
{
name: "with default options",
cli: "",
wantsErr: false,
wantsOpts: LogOptions{
Entries: 5,
Start: 1,
LogType: "all",
IndexName: nil,
},
},
{
name: "with 69 entries, starting at 420, type query, filtered by index foo",
cli: "--entries 69 --start 420 --type query --index foo",
wantsErr: false,
wantsOpts: LogOptions{
Entries: 69,
Start: 420,
LogType: "query",
IndexName: &testIndexName,
},
},
}

for _, tt := range tests {
f := &cmdutil.Factory{}
var opts *LogOptions
cmd := NewListCmd(f, func(o *LogOptions) error {
opts = o
return nil
})
args, err := shlex.Split(tt.cli)
require.NoError(t, err)
cmd.SetArgs(args)
_, err = cmd.ExecuteC()
if tt.wantsErr {
assert.Error(t, err)
return
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.wantsOpts.Entries, opts.Entries)
assert.Equal(t, tt.wantsOpts.Start, opts.Start)
assert.Equal(t, tt.wantsOpts.LogType, opts.LogType)
assert.Equal(t, tt.wantsOpts.IndexName, opts.IndexName)
}
}
19 changes: 19 additions & 0 deletions pkg/cmd/logs/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package logs

import (
"github.com/spf13/cobra"

"github.com/algolia/cli/pkg/cmd/logs/list"
"github.com/algolia/cli/pkg/cmdutil"
)

// NewLogsCmd returns a new command for retrieving logs
func NewLogsCmd(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "logs",
Short: "Retrieve your Algolia Search API logs",
}

cmd.AddCommand(list.NewListCmd(f, nil))
return cmd
}
14 changes: 8 additions & 6 deletions pkg/cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/algolia/cli/pkg/cmd/events"
"github.com/algolia/cli/pkg/cmd/factory"
"github.com/algolia/cli/pkg/cmd/indices"
"github.com/algolia/cli/pkg/cmd/logs"
"github.com/algolia/cli/pkg/cmd/objects"
"github.com/algolia/cli/pkg/cmd/open"
"github.com/algolia/cli/pkg/cmd/profile"
Expand Down Expand Up @@ -99,16 +100,17 @@ func NewRootCmd(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(open.NewOpenCmd(f))

// API related commands
cmd.AddCommand(search.NewSearchCmd(f))
cmd.AddCommand(apikeys.NewAPIKeysCmd(f))
cmd.AddCommand(crawler.NewCrawlersCmd(f))
cmd.AddCommand(dictionary.NewDictionaryCmd(f))
cmd.AddCommand(events.NewEventsCmd(f))
cmd.AddCommand(indices.NewIndicesCmd(f))
cmd.AddCommand(logs.NewLogsCmd(f))
cmd.AddCommand(objects.NewObjectsCmd(f))
cmd.AddCommand(apikeys.NewAPIKeysCmd(f))
cmd.AddCommand(settings.NewSettingsCmd(f))
cmd.AddCommand(rules.NewRulesCmd(f))
cmd.AddCommand(search.NewSearchCmd(f))
cmd.AddCommand(settings.NewSettingsCmd(f))
cmd.AddCommand(synonyms.NewSynonymsCmd(f))
cmd.AddCommand(dictionary.NewDictionaryCmd(f))
cmd.AddCommand(events.NewEventsCmd(f))
cmd.AddCommand(crawler.NewCrawlersCmd(f))

return cmd
}
Expand Down
102 changes: 102 additions & 0 deletions pkg/cmdutil/flags.go
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has two helpers NilStringFlag and StringEnumFlag which helps with certain string flags for better validation and completion. I think this might be useful for other commands as well (especialy StringEnumFlag).

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package cmdutil

import (
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// Pretty much the whole code in this file is copied from the GitHub CLI

// NilStringFlag defines a new flag with a string pointer receiver.
// This helps distinguishing `--flag ""` from not setting the flag at all.
func NilStringFlag(
cmd *cobra.Command,
p **string,
name string,
shorthand string,
usage string,
) *pflag.Flag {
return cmd.Flags().VarPF(newStringValue(p), name, shorthand, usage)
}

// StringEnumFlag defines a new string flag restricted to allowed options
func StringEnumFlag(
cmd *cobra.Command,
p *string,
name, shorthand, defaultValue string,
options []string,
usage string,
) *pflag.Flag {
*p = defaultValue
val := &enumValue{string: p, options: options}
f := cmd.Flags().
VarPF(val, name, shorthand, fmt.Sprintf("%s: %s", usage, formatValuesForUsageDocs(options)))
_ = cmd.RegisterFlagCompletionFunc(
name,
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return options, cobra.ShellCompDirectiveNoFileComp
},
)
return f
}

type enumValue struct {
string *string
options []string
}

func (e *enumValue) Set(value string) error {
if !isIncluded(value, e.options) {
return fmt.Errorf("valid values are %s", formatValuesForUsageDocs(e.options))
}
*e.string = value
return nil
}

func (e *enumValue) String() string {
return *e.string
}

func (e *enumValue) Type() string {
return "string"
}

func isIncluded(value string, opts []string) bool {
for _, opt := range opts {
if strings.EqualFold(opt, value) {
return true
}
}
return false
}

func formatValuesForUsageDocs(values []string) string {
return fmt.Sprintf("{%s}", strings.Join(values, "|"))
}

type stringValue struct {
string **string
}

func (s *stringValue) Set(value string) error {
*s.string = &value
return nil
}

func (s *stringValue) String() string {
if s.string == nil || *s.string == nil {
return ""
}
return **s.string
}

func (s *stringValue) Type() string {
return "string"
}

func newStringValue(p **string) *stringValue {
return &stringValue{p}
}
Loading