diff --git a/internal/cli/logs.go b/internal/cli/logs.go index c8b164534..2a1a98c79 100644 --- a/internal/cli/logs.go +++ b/internal/cli/logs.go @@ -9,6 +9,11 @@ import ( "github.com/spf13/cobra" ) +// Besides the limitation of 100 log events per request to retrieve logs, +// we may only paginate through up to 1000 search results. +// https://auth0.com/docs/logs/retrieve-log-events-using-mgmt-api#limitations +const logsPerPageLimit = 100 + var ( logsFilter = Flag{ Name: "Filter", @@ -63,7 +68,7 @@ func listLogsCmd(cli *cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { list, err := getLatestLogs(cli, inputs.Num, inputs.Filter) if err != nil { - return fmt.Errorf("An unexpected error occurred while getting logs: %v", err) + return fmt.Errorf("failed to get logs: %w", err) } hasFilter := inputs.Filter != "" @@ -100,45 +105,43 @@ func tailLogsCmd(cli *cli) *cobra.Command { auth0 logs tail --filter "type:f" # See the full list of type codes at https://auth0.com/docs/logs/log-event-type-codes auth0 logs tail -n 100`, RunE: func(cmd *cobra.Command, args []string) error { - lastLogID := "" list, err := getLatestLogs(cli, inputs.Num, inputs.Filter) if err != nil { - return fmt.Errorf("An unexpected error occurred while getting logs: %v", err) + return fmt.Errorf("failed to get logs: %w", err) } - // TODO(cyx): This is a hack for now to make the - // streaming work faster. - // - // Create a `set` to detect duplicates clientside. - set := make(map[string]struct{}) - list = dedupeLogs(list, set) + logsCh := make(chan []*management.Log) + var lastLogID string if len(list) > 0 { lastLogID = list[len(list)-1].GetLogID() } - logsCh := make(chan []*management.Log) + // Create a `set` to detect duplicates clientside. + set := make(map[string]struct{}) + list = dedupeLogs(list, set) - go func() { - // This is pretty important and allows - // us to close / terminate the command. + go func(lastLogID string) { defer close(logsCh) for { queryParams := []management.RequestOption{ - management.Query(fmt.Sprintf("log_id:[%s TO *]", lastLogID)), management.Parameter("page", "0"), management.Parameter("per_page", "100"), management.Parameter("sort", "date:-1"), } + if lastLogID != "" { + queryParams = append(queryParams, management.Query(fmt.Sprintf("log_id:[%s TO *]", lastLogID))) + } + if inputs.Filter != "" { queryParams = append(queryParams, management.Query(inputs.Filter)) } - list, err = cli.api.Log.List(queryParams...) + list, err := cli.api.Log.List(queryParams...) if err != nil { - cli.renderer.Errorf("An unexpected error occurred while getting logs: %v", err) + cli.renderer.Errorf("Failed to get latest logs: %v", err) return } @@ -147,12 +150,12 @@ func tailLogsCmd(cli *cli) *cobra.Command { lastLogID = list[len(list)-1].GetLogID() } - if len(list) < 90 { - // Not a lot is happening, sleep on it - time.Sleep(1 * time.Second) + if len(list) < logsPerPageLimit { + // Not a lot is happening, sleep on it. + time.Sleep(time.Second) } } - }() + }(lastLogID) cli.renderer.LogTail(list, logsCh, !cli.debug) return nil @@ -168,11 +171,8 @@ func tailLogsCmd(cli *cli) *cobra.Command { func getLatestLogs(cli *cli, n int, filter string) ([]*management.Log, error) { page := 0 perPage := n - - if perPage > 1000 { - // Pagination max out at 1000 entries in total - // https://auth0.com/docs/logs/retrieve-log-events-using-mgmt-api#limitations - perPage = 1000 + if perPage > logsPerPageLimit { + perPage = logsPerPageLimit } queryParams := []management.RequestOption{ diff --git a/internal/cli/logs_test.go b/internal/cli/logs_test.go index 50b62f886..14a6a1b7f 100644 --- a/internal/cli/logs_test.go +++ b/internal/cli/logs_test.go @@ -1,16 +1,109 @@ package cli import ( + "bytes" + "fmt" "testing" "time" "github.com/auth0/go-auth0/management" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/auth0/mock" + "github.com/auth0/auth0-cli/internal/display" ) +func TestTailLogsCommand(t *testing.T) { + t.Run("it returns an error when it fails to get the logs on the first request", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logsAPI := mock.NewMockLogAPI(ctrl) + logsAPI.EXPECT(). + List(gomock.Any()). + Return(nil, fmt.Errorf("generic error")) + + cli := &cli{ + api: &auth0.API{Log: logsAPI}, + } + + cmd := tailLogsCmd(cli) + cmd.SetArgs([]string{"--number", "90", "--filter", "user_id:123"}) + err := cmd.Execute() + + assert.EqualError(t, err, "failed to get logs: generic error") + }) + + t.Run("it returns an error when it fails to get the logs on the 3rd request", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + logsAPI := mock.NewMockLogAPI(ctrl) + logsAPI.EXPECT(). + List(gomock.Any()). + Return( + []*management.Log{ + { + LogID: auth0.String("354234"), + Type: auth0.String("sapi"), + Description: auth0.String("Update branding settings"), + }, + }, + nil, + ) + + logsAPI.EXPECT(). + List(gomock.Any()). + Return( + []*management.Log{ + { + LogID: auth0.String("354234"), + Type: auth0.String("sapi"), + Description: auth0.String("Update branding settings"), + }, + { + LogID: auth0.String("354236"), + Type: auth0.String("sapi"), + Description: auth0.String("Update tenant settings"), + }, + }, + nil, + ) + + logsAPI.EXPECT(). + List(gomock.Any()). + Return(nil, fmt.Errorf("generic error")) + + expectedResult := `TYPE DESCRIPTION DATE CONNECTION CLIENT +API Operation Update branding settings Jan 01 00:00:00.000 N/A N/A +` + + message := &bytes.Buffer{} + result := &bytes.Buffer{} + cli := &cli{ + renderer: &display.Renderer{ + Tenant: "auth0-cli-tests.eu.auth0.com", + MessageWriter: message, + ResultWriter: result, + }, + api: &auth0.API{Log: logsAPI}, + } + + cmd := tailLogsCmd(cli) + cmd.SetArgs([]string{"--number", "90", "--filter", "user_id:123"}) + err := cmd.Execute() + assert.NoError(t, err) + + assert.Contains(t, message.String(), "auth0-cli-tests.eu.auth0.com") // Ensure we display the tenant name. + assert.Contains(t, message.String(), "logs") // Ensure header is set in output. + assert.Contains(t, message.String(), "Failed to get latest logs: generic error") + assert.Equal(t, expectedResult, result.String()) + }) +} + func TestDedupeLogs(t *testing.T) { t.Run("removes duplicate logs and sorts by date asc", func(t *testing.T) { logs := []*management.Log{