Skip to content

Commit

Permalink
Introduce Log Tailing command (stripe#57)
Browse files Browse the repository at this point in the history
* Log tailing command in top-level command dir

* Logs command in subdir

* Add websocket test for request log events

* output formatting

* new format for created_at

* remove empty test file

* add flag for json formatting'

* switch to aurora

* new linting fixes

* adjust imports

* fix new linting errors

* implement filter args

* filter out sessions logs

* fix linting

* some PR feedback and linkify request id

* add requestlogid comment clarification

* move requestlogid comment

* rename logs packages

* fix help messages

* fix linting

* shorten short message

* Add UTC to displayed time

* Add default value for missing path

* --help formatting (stripe#76)

* new help format

* API -> Stripe

* new arg validators

* add source validator

* add validators to log tail execution

* Add new filters and filter lists

* connect warning message

* logs tail help formatting

* fix missing quote

* Address partial tomer feedback with help messages

* remove unnecessary todo comment

* convert status code type to be specified as 2XX,4XX,5XX

* local timezone formatting

* fix X replace bug

* rebase on master and fix api key call
  • Loading branch information
aarongreen-stripe authored Aug 12, 2019
1 parent d7ed39d commit c6630f4
Show file tree
Hide file tree
Showing 13 changed files with 773 additions and 21 deletions.
32 changes: 32 additions & 0 deletions pkg/cmd/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package cmd

import (
"github.com/spf13/cobra"

logs "github.com/stripe/stripe-cli/pkg/cmd/logs"
"github.com/stripe/stripe-cli/pkg/config"
"github.com/stripe/stripe-cli/pkg/validators"
)

// LogsCmd is a wrapper for the base logs command
type LogsCmd struct {
Cmd *cobra.Command
cfg *config.Config
}

func newLogsCmd(config *config.Config) *LogsCmd {
logsCmd := &LogsCmd{
cfg: config,
}

logsCmd.Cmd = &cobra.Command{
Use: "logs",
Args: validators.NoArgs,
Short: "Interact with Stripe request logs",
Long: `The logs command allows you to interact with your API request logs from Stripe. The first supported feature is log tailing, which allows you to view your API request logs in real-time.`,
}

logsCmd.Cmd.AddCommand(logs.NewTailCmd(logsCmd.cfg).Cmd)

return logsCmd
}
202 changes: 202 additions & 0 deletions pkg/cmd/logs/tail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package logs

import (
"fmt"
"strings"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/stripe/stripe-cli/pkg/config"
logTailing "github.com/stripe/stripe-cli/pkg/logtailing"
"github.com/stripe/stripe-cli/pkg/validators"
)

const requestLogsWebSocketFeature = "request_logs"

// TailCmd wraps the configuration for the tail command
type TailCmd struct {
apiBaseURL string
cfg *config.Config
Cmd *cobra.Command
format string
LogFilters *logTailing.LogFilters
noWSS bool
}

// NewTailCmd creates and initializes the tail command for the logs package
func NewTailCmd(config *config.Config) *TailCmd {
tailCmd := &TailCmd{
cfg: config,
LogFilters: &logTailing.LogFilters{},
}

tailCmd.Cmd = &cobra.Command{
Use: "tail",
Args: validators.NoArgs,
Short: "Tails request logs created by making API requests to Stripe.",
Long: fmt.Sprintf(`The tail command lets you tail API request logs from Stripe.
The command establishes a direct connection with Stripe to send the request logs to your local machine.
Watch for all request logs sent from Stripe:
$ stripe logs tail`),
RunE: tailCmd.runTailCmd,
}

tailCmd.Cmd.Flags().StringVar(
&tailCmd.format,
"format",
"",
`Specifies the output format of request logs
Acceptable values:
'JSON' - Output logs in JSON format`,
)

// Log filters
tailCmd.Cmd.Flags().StringSliceVar(
&tailCmd.LogFilters.FilterAccount,
"filter-account",
[]string{},
`*CONNECT ONLY* Filter request logs by source and destination account
Acceptable values:
'connect_in' - Incoming connect requests
'connect_out' - Outgoing connect requests
'self' - Non-connect requests`,
)
tailCmd.Cmd.Flags().StringSliceVar(&tailCmd.LogFilters.FilterIPAddress, "filter-ip-address", []string{}, "Filter request logs by ip address")
tailCmd.Cmd.Flags().StringSliceVar(
&tailCmd.LogFilters.FilterHTTPMethod,
"filter-http-method",
[]string{},
`Filter request logs by http method
Acceptable values:
'GET' - HTTP get requests
'POST' - HTTP post requests
'DELETE' - HTTP delete requests`,
)
tailCmd.Cmd.Flags().StringSliceVar(&tailCmd.LogFilters.FilterRequestPath, "filter-request-path", []string{}, "Filter request logs by request path")
tailCmd.Cmd.Flags().StringSliceVar(
&tailCmd.LogFilters.FilterRequestStatus,
"filter-request-status",
[]string{},
`Filter request logs by request status
Acceptable values:
'SUCCEEDED' - Requests that succeeded (status codes 200, 201, 202)
'FAILED' - Requests that failed`,
)
tailCmd.Cmd.Flags().StringSliceVar(
&tailCmd.LogFilters.FilterSource,
"filter-source",
[]string{},
`Filter request logs by source
Acceptable values:
'API' - Requests that came through the Stripe API
'DASHBOARD' - Requests that came through the Stripe Dashboard`,
)
tailCmd.Cmd.Flags().StringSliceVar(&tailCmd.LogFilters.FilterStatusCode, "filter-status-code", []string{}, "Filter request logs by status code")
tailCmd.Cmd.Flags().StringSliceVar(
&tailCmd.LogFilters.FilterStatusCodeType,
"filter-status-code-type",
[]string{},
`Filter request logs by status code type
Acceptable values:
'2XX' - All 2XX status codes
'4XX' - All 4XX status codes
'5XX' - All 5XX status codes`,
)

// Hidden configuration flags, useful for dev/debugging
tailCmd.Cmd.Flags().StringVar(&tailCmd.apiBaseURL, "api-base", "", "Sets the API base URL")
tailCmd.Cmd.Flags().MarkHidden("api-base") // #nosec G104

tailCmd.Cmd.Flags().BoolVar(&tailCmd.noWSS, "no-wss", false, "Force unencrypted ws:// protocol instead of wss://")
tailCmd.Cmd.Flags().MarkHidden("no-wss") // #nosec G104

return tailCmd
}

func (tailCmd *TailCmd) runTailCmd(cmd *cobra.Command, args []string) error {
err := tailCmd.validateArgs()
if err != nil {
return err
}

err = tailCmd.convertArgs()
if err != nil {
return err
}

deviceName, err := tailCmd.cfg.Profile.GetDeviceName()
if err != nil {
return err
}

key, err := tailCmd.cfg.Profile.GetAPIKey()
if err != nil {
return err
}

tailer := logTailing.New(&logTailing.Config{
APIBaseURL: tailCmd.apiBaseURL,
DeviceName: deviceName,
Filters: tailCmd.LogFilters,
Key: key,
Log: log.StandardLogger(),
NoWSS: tailCmd.noWSS,
OutputFormat: tailCmd.format,
WebSocketFeature: requestLogsWebSocketFeature,
})

err = tailer.Run()
if err != nil {
return err
}

return nil
}

func (tailCmd *TailCmd) validateArgs() error {
err := validators.CallNonEmptyArray(validators.Account, tailCmd.LogFilters.FilterAccount)
if err != nil {
return err
}

err = validators.CallNonEmptyArray(validators.HTTPMethod, tailCmd.LogFilters.FilterHTTPMethod)
if err != nil {
return err
}

err = validators.CallNonEmptyArray(validators.StatusCode, tailCmd.LogFilters.FilterStatusCode)
if err != nil {
return err
}

err = validators.CallNonEmptyArray(validators.StatusCodeType, tailCmd.LogFilters.FilterStatusCodeType)
if err != nil {
return err
}

err = validators.CallNonEmptyArray(validators.RequestSource, tailCmd.LogFilters.FilterSource)
if err != nil {
return err
}

err = validators.CallNonEmptyArray(validators.RequestStatus, tailCmd.LogFilters.FilterRequestStatus)
if err != nil {
return err
}

return nil
}

func (tailCmd *TailCmd) convertArgs() error {
// The backend expects to receive the status code type as a string representing the start of the range (e.g., '200')
if len(tailCmd.LogFilters.FilterStatusCodeType) > 0 {
for i, code := range tailCmd.LogFilters.FilterStatusCodeType {
tailCmd.LogFilters.FilterStatusCodeType[i] = strings.ReplaceAll(strings.ToUpper(code), "X", "0")
}
}

return nil
}
17 changes: 12 additions & 5 deletions pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ var rootCmd = &cobra.Command{
SilenceUsage: true,
SilenceErrors: true,
Annotations: map[string]string{
"get": "api",
"post": "api",
"delete": "api",
"get": "http",
"post": "http",
"delete": "http",
"trigger": "webhooks",
"listen": "webhooks",
"logs": "stripe",
"status": "stripe",
},
Version: version.Version,
Short: "A CLI to help you integrate Stripe with your application",
Expand Down Expand Up @@ -60,12 +62,15 @@ func Execute() {
%s
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{if .Annotations}}
%s{{range $index, $cmd := .Commands}}{{if (eq (index $.Annotations $cmd.Name) "api")}}
%s{{range $index, $cmd := .Commands}}{{if (eq (index $.Annotations $cmd.Name) "http")}}
{{rpad $cmd.Name $cmd.NamePadding }} {{$cmd.Short}}{{end}}{{end}}
%s{{range $index, $cmd := .Commands}}{{if (eq (index $.Annotations $cmd.Name) "webhooks")}}
{{rpad $cmd.Name $cmd.NamePadding }} {{$cmd.Short}}{{end}}{{end}}
%s{{range $index, $cmd := .Commands}}{{if (eq (index $.Annotations $cmd.Name) "stripe")}}
{{rpad $cmd.Name $cmd.NamePadding }} {{$cmd.Short}}{{end}}{{end}}
%s{{range $index, $cmd := .Commands}}{{if (not (index $.Annotations $cmd.Name))}}
{{rpad $cmd.Name $cmd.NamePadding }} {{$cmd.Short}}{{end}}{{end}}{{else}}
Expand All @@ -86,8 +91,9 @@ Use "{{.CommandPath}} [command] --help" for more information about a command.{{e
ansi.Bold("Usage:"),
ansi.Bold("Aliases:"),
ansi.Bold("Examples:"),
ansi.Bold("API commands:"),
ansi.Bold("HTTP commands:"),
ansi.Bold("Webhook commands:"),
ansi.Bold("Stripe commands:"),
ansi.Bold("Other commands:"),
ansi.Bold("Available commands:"),
ansi.Bold("Flags:"),
Expand Down Expand Up @@ -127,4 +133,5 @@ func init() {
rootCmd.AddCommand(newStatusCmd().cmd)
rootCmd.AddCommand(newTriggerCmd().cmd)
rootCmd.AddCommand(newVersionCmd().cmd)
rootCmd.AddCommand(newLogsCmd(&Config).Cmd)
}
Loading

0 comments on commit c6630f4

Please sign in to comment.