diff --git a/tools/cli/README.md b/tools/cli/README.md index e9c30e9ecd1..4081a6b0fe4 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -90,9 +90,12 @@ User need to run `show` to view workflow history/progress. ./cadence workflow showid 3ea6b242-b23c-4279-bb13-f215661b4717 ``` -- List open or closed workflow executions +- List closed or open workflow executions ``` ./cadence workflow list + +# default will only show one page, to view more items, use --more flag +./cadence workflow list -m ``` - Query workflow execution diff --git a/tools/cli/commands.go b/tools/cli/commands.go index 5306a8fca89..12cdae34db0 100644 --- a/tools/cli/commands.go +++ b/tools/cli/commands.go @@ -21,7 +21,6 @@ package cli import ( - "bufio" "bytes" "context" "encoding/json" @@ -78,6 +77,8 @@ const ( FlagReasonWithAlias = FlagReason + ", re" FlagOpen = "open" FlagOpenWithAlias = FlagOpen + ", op" + FlagMore = "more" + FlagMoreWithAlias = FlagMore + ", m" FlagPageSize = "pagesize" FlagPageSizeWithAlias = FlagPageSize + ", ps" FlagEarliestTime = "earliest_time" @@ -86,6 +87,8 @@ const ( FlagLatestTimeWithAlias = FlagLatestTime + ", lt" FlagPrintRawTime = "print_raw_time" FlagPrintRawTimeWithAlias = FlagPrintRawTime + ", prt" + FlagPrintDateTime = "print_datetime" + FlagPrintDateTimeWithAlias = FlagPrintDateTime + ", pdt" FlagDescription = "description" FlagDescriptionWithAlias = FlagDescription + ", desc" FlagOwnerEmail = "owner_email" @@ -106,11 +109,14 @@ const ( localHostPort = "127.0.0.1:7933" maxOutputStringLength = 200 // max length for output string + maxWorkflowTypeLength = 32 // max item length for output workflow type in table - defaultTimeFormat = time.RFC3339 // used for converting UnixNano to string like 2018-02-15T16:16:36-08:00 + defaultTimeFormat = "15:04:05" // used for converting UnixNano to string like 16:16:36 (only time) + defaultDateTimeFormat = time.RFC3339 // used for converting UnixNano to string like 2018-02-15T16:16:36-08:00 defaultDomainRetentionDays = 3 defaultContextTimeoutForLongPoll = 2 * time.Minute defaultDecisionTimeoutInSeconds = 10 + defaultPageSizeForList = 500 ) // For color output to terminal @@ -118,6 +124,8 @@ var ( colorRed = color.New(color.FgRed).SprintFunc() colorMagenta = color.New(color.FgMagenta).SprintFunc() colorGreen = color.New(color.FgGreen).SprintFunc() + + tableHeaderBlue = tablewriter.Colors{tablewriter.FgHiBlueColor} ) // cBuilder is used to create cadence clients @@ -297,6 +305,7 @@ func ShowHistoryWithWID(c *cli.Context) { func showHistoryHelper(c *cli.Context, wid, rid string) { wfClient := getWorkflowClient(c) + printDateTime := c.Bool(FlagPrintDateTime) printRawTime := c.Bool(FlagPrintRawTime) outputFileName := c.String(FlagOutputFilename) @@ -307,13 +316,19 @@ func showHistoryHelper(c *cli.Context, wid, rid string) { ExitIfError(err) } + table := tablewriter.NewWriter(os.Stdout) + table.SetBorder(false) + table.SetColumnSeparator("") for _, e := range history.Events { if printRawTime { - fmt.Printf("%d, %d, %s\n", e.GetEventId(), e.GetTimestamp(), HistoryEventToString(e)) + table.Append([]string{strconv.FormatInt(e.GetEventId(), 10), strconv.FormatInt(e.GetTimestamp(), 10), ColorEvent(e), HistoryEventToString(e)}) + } else if printDateTime { + table.Append([]string{strconv.FormatInt(e.GetEventId(), 10), convertTime(e.GetTimestamp(), false), ColorEvent(e), HistoryEventToString(e)}) } else { - fmt.Printf("%d, %s, %s\n", e.GetEventId(), convertTime(e.GetTimestamp()), HistoryEventToString(e)) + table.Append([]string{strconv.FormatInt(e.GetEventId(), 10), ColorEvent(e), HistoryEventToString(e)}) } } + table.Render() if outputFileName != "" { serializer := &JSONHistorySerializer{} @@ -456,7 +471,7 @@ func RunWorkflow(c *cli.Context) { removePrevious2LinesFromTerminal() isTimeElapseExist = false } - fmt.Printf(" %d, %s, %v\n", event.GetEventId(), convertTime(event.GetTimestamp()), event.GetEventType()) + fmt.Printf(" %d, %s, %v\n", event.GetEventId(), convertTime(event.GetTimestamp(), false), event.GetEventType()) lastEvent = event } doneChan <- true @@ -598,52 +613,110 @@ func queryWorkflowHelper(c *cli.Context, queryType string) { // ListWorkflow list workflow executions based on filters func ListWorkflow(c *cli.Context) { + more := c.Bool(FlagMore) + pageSize := c.Int(FlagPageSize) + + table := createTableForListWorkflow(false) + prepareTable := listWorkflow(c, table) + + if !more { // default mode only show one page items + prepareTable(nil) + table.Render() + } else { // require input Enter to view next page + var resultSize int + var nextPageToken []byte + for { + nextPageToken, resultSize = prepareTable(nextPageToken) + table.Render() + table.ClearRows() + + if resultSize < pageSize { + break + } + + fmt.Printf("Press %s to show next page, press %s to quit: ", + color.GreenString("Enter"), color.RedString("any other key then Enter")) + var input string + fmt.Scanln(&input) + if strings.Trim(input, " ") == "" { + continue + } else { + break + } + } + } +} + +// ListAllWorkflow list all workflow executions based on filters +func ListAllWorkflow(c *cli.Context) { + table := createTableForListWorkflow(true) + prepareTable := listWorkflow(c, table) + var resultSize int + var nextPageToken []byte + for { + nextPageToken, resultSize = prepareTable(nextPageToken) + if resultSize < defaultPageSizeForList { + break + } + } + table.Render() +} + +func createTableForListWorkflow(listAll bool) *tablewriter.Table { + table := tablewriter.NewWriter(os.Stdout) + table.SetBorder(false) + table.SetColumnSeparator("|") + table.SetHeader([]string{"Workflow Type", "Workflow ID", "Run ID", "Start Time", "End Time"}) + if !listAll { // color is only friendly to ANSI terminal + table.SetHeaderColor(tableHeaderBlue, tableHeaderBlue, tableHeaderBlue, tableHeaderBlue, tableHeaderBlue) + } + table.SetHeaderLine(false) + return table +} + +func listWorkflow(c *cli.Context, table *tablewriter.Table) func([]byte) ([]byte, int) { wfClient := getWorkflowClient(c) queryOpen := c.Bool(FlagOpen) - pageSize := c.Int(FlagPageSize) earliestTime := parseTime(c.String(FlagEarliestTime), 0) latestTime := parseTime(c.String(FlagLatestTime), time.Now().UnixNano()) workflowID := c.String(FlagWorkflowID) workflowType := c.String(FlagWorkflowType) printRawTime := c.Bool(FlagPrintRawTime) + printDateTime := c.Bool(FlagPrintDateTime) + pageSize := c.Int(FlagPageSize) + if pageSize <= 0 { + pageSize = defaultPageSizeForList + } if len(workflowID) > 0 && len(workflowType) > 0 { ExitIfError(errors.New("you can filter on workflow_id or workflow_type, but not on both")) } - reader := bufio.NewReader(os.Stdin) - var result []*s.WorkflowExecutionInfo - var nextPageToken []byte - for { + prepareTable := func(next []byte) ([]byte, int) { + var result []*s.WorkflowExecutionInfo + var nextPageToken []byte if queryOpen { - result, nextPageToken = listOpenWorkflow(wfClient, pageSize, earliestTime, latestTime, workflowID, workflowType, nextPageToken) + result, nextPageToken = listOpenWorkflow(wfClient, pageSize, earliestTime, latestTime, workflowID, workflowType, next) } else { - result, nextPageToken = listClosedWorkflow(wfClient, pageSize, earliestTime, latestTime, workflowID, workflowType, nextPageToken) + result, nextPageToken = listClosedWorkflow(wfClient, pageSize, earliestTime, latestTime, workflowID, workflowType, next) } for _, e := range result { - fmt.Printf("%s, -w %s -r %s", e.Type.GetName(), e.Execution.GetWorkflowId(), e.Execution.GetRunId()) + var startTime, closeTime string if printRawTime { - fmt.Printf(" [%d, %d]\n", e.GetStartTime(), e.GetCloseTime()) + startTime = fmt.Sprintf("%d", e.GetStartTime()) + closeTime = fmt.Sprintf("%d", e.GetCloseTime()) } else { - fmt.Printf(" [%s, %s]\n", convertTime(e.GetStartTime()), convertTime(e.GetCloseTime())) + startTime = convertTime(e.GetStartTime(), !printDateTime) + closeTime = convertTime(e.GetCloseTime(), !printDateTime) } + table.Append([]string{trimWorkflowType(e.Type.GetName()), e.Execution.GetWorkflowId(), e.Execution.GetRunId(), startTime, closeTime}) } - if len(result) < pageSize { - break - } - - fmt.Println("Press C then Enter to show more result, press any other key then Enter to quit: ") - input, _ := reader.ReadString('\n') - c := []byte(input)[0] - if c == 'C' || c == 'c' { - continue - } else { - break - } + return nextPageToken, len(result) } + return prepareTable } func listOpenWorkflow(client client.Client, pageSize int, earliestTime, latestTime int64, workflowID, workflowType string, nextPageToken []byte) ([]*s.WorkflowExecutionInfo, []byte) { @@ -718,9 +791,11 @@ func DescribeTaskList(c *cli.Context) { table := tablewriter.NewWriter(os.Stdout) table.SetBorder(false) table.SetColumnSeparator("|") - table.Append([]string{"Poller Identity", "Last Access Time"}) + table.SetHeader([]string{"Poller Identity", "Last Access Time"}) + table.SetHeaderLine(false) + table.SetHeaderColor(tableHeaderBlue, tableHeaderBlue) for _, poller := range pollers { - table.Append([]string{poller.GetIdentity(), convertTime(poller.GetLastAccessTime())}) + table.Append([]string{poller.GetIdentity(), convertTime(poller.GetLastAccessTime(), false)}) } table.Render() } @@ -779,9 +854,15 @@ func getRequiredGlobalOption(c *cli.Context, optionName string) string { return value } -func convertTime(unixNano int64) string { - t2 := time.Unix(0, unixNano) - return t2.Format(defaultTimeFormat) +func convertTime(unixNano int64, onlyTime bool) string { + t := time.Unix(0, unixNano) + var result string + if onlyTime { + result = t.Format(defaultTimeFormat) + } else { + result = t.Format(defaultDateTimeFormat) + } + return result } func parseTime(timeStr string, defaultValue int64) int64 { @@ -903,3 +984,18 @@ func printRunStatus(event *s.HistoryEvent) { fmt.Printf(" Detail: %s\n", string(event.WorkflowExecutionCanceledEventAttributes.Details)) } } + +// in case workflow type is too long to show in table, trim it like .../example.Workflow +func trimWorkflowType(str string) string { + res := str + if len(str) >= maxWorkflowTypeLength { + items := strings.Split(str, "/") + res = items[len(items)-1] + if len(res) >= maxWorkflowTypeLength { + res = "..." + res[len(res)-maxWorkflowTypeLength:] + } else { + res = ".../" + res + } + } + return res +} diff --git a/tools/cli/util.go b/tools/cli/util.go index 493d52d7ce6..cea9aec5b5f 100644 --- a/tools/cli/util.go +++ b/tools/cli/util.go @@ -27,6 +27,7 @@ import ( "fmt" "reflect" + "github.com/fatih/color" s "go.uber.org/cadence/.gen/go/shared" "go.uber.org/cadence/client" ) @@ -195,7 +196,7 @@ func HistoryEventToString(e *s.HistoryEvent) string { data = e } - return e.GetEventType().String() + ": " + anyToString(data) + return anyToString(data) } func anyToString(d interface{}) string { @@ -219,7 +220,13 @@ func anyToString(d interface{}) string { if buf.Len() > 1 { buf.WriteString(", ") } - buf.WriteString(fmt.Sprintf("%s:%s", t.Field(i).Name, fieldValue)) + fieldName := t.Field(i).Name + if fieldName == "Reason" || fieldName == "Details" || fieldName == "Cause" { + buf.WriteString(fmt.Sprintf("%s:%s", color.RedString(fieldName), color.MagentaString(fieldValue))) + } else { + buf.WriteString(fmt.Sprintf("%s:%s", fieldName, fieldValue)) + } + } buf.WriteString(")") return buf.String() @@ -245,3 +252,140 @@ func valueToString(v reflect.Value) string { return fmt.Sprint(v.Interface()) } } + +// ColorEvent takes an event and return string with color +// Event with color mapping rules: +// Failed - red +// Timeout - yellow +// Canceled - magenta +// Completed - green +// Started - blue +// Others - default (white/black) +func ColorEvent(e *s.HistoryEvent) string { + var data string + switch e.GetEventType() { + case s.EventTypeWorkflowExecutionStarted: + data = color.BlueString(e.EventType.String()) + + case s.EventTypeWorkflowExecutionCompleted: + data = color.GreenString(e.EventType.String()) + + case s.EventTypeWorkflowExecutionFailed: + data = color.RedString(e.EventType.String()) + + case s.EventTypeWorkflowExecutionTimedOut: + data = color.YellowString(e.EventType.String()) + + case s.EventTypeDecisionTaskScheduled: + data = e.EventType.String() + + case s.EventTypeDecisionTaskStarted: + data = e.EventType.String() + + case s.EventTypeDecisionTaskCompleted: + data = e.EventType.String() + + case s.EventTypeDecisionTaskTimedOut: + data = color.YellowString(e.EventType.String()) + + case s.EventTypeActivityTaskScheduled: + data = e.EventType.String() + + case s.EventTypeActivityTaskStarted: + data = e.EventType.String() + + case s.EventTypeActivityTaskCompleted: + data = e.EventType.String() + + case s.EventTypeActivityTaskFailed: + data = color.RedString(e.EventType.String()) + + case s.EventTypeActivityTaskTimedOut: + data = color.YellowString(e.EventType.String()) + + case s.EventTypeActivityTaskCancelRequested: + data = e.EventType.String() + + case s.EventTypeRequestCancelActivityTaskFailed: + data = color.RedString(e.EventType.String()) + + case s.EventTypeActivityTaskCanceled: + data = e.EventType.String() + + case s.EventTypeTimerStarted: + data = e.EventType.String() + + case s.EventTypeTimerFired: + data = e.EventType.String() + + case s.EventTypeCancelTimerFailed: + data = color.RedString(e.EventType.String()) + + case s.EventTypeTimerCanceled: + data = color.MagentaString(e.EventType.String()) + + case s.EventTypeWorkflowExecutionCancelRequested: + data = e.EventType.String() + + case s.EventTypeWorkflowExecutionCanceled: + data = color.MagentaString(e.EventType.String()) + + case s.EventTypeRequestCancelExternalWorkflowExecutionInitiated: + data = e.EventType.String() + + case s.EventTypeRequestCancelExternalWorkflowExecutionFailed: + data = color.RedString(e.EventType.String()) + + case s.EventTypeExternalWorkflowExecutionCancelRequested: + data = e.EventType.String() + + case s.EventTypeMarkerRecorded: + data = e.EventType.String() + + case s.EventTypeWorkflowExecutionSignaled: + data = e.EventType.String() + + case s.EventTypeWorkflowExecutionTerminated: + data = e.EventType.String() + + case s.EventTypeWorkflowExecutionContinuedAsNew: + data = e.EventType.String() + + case s.EventTypeStartChildWorkflowExecutionInitiated: + data = e.EventType.String() + + case s.EventTypeStartChildWorkflowExecutionFailed: + data = color.RedString(e.EventType.String()) + + case s.EventTypeChildWorkflowExecutionStarted: + data = color.BlueString(e.EventType.String()) + + case s.EventTypeChildWorkflowExecutionCompleted: + data = color.GreenString(e.EventType.String()) + + case s.EventTypeChildWorkflowExecutionFailed: + data = color.RedString(e.EventType.String()) + + case s.EventTypeChildWorkflowExecutionCanceled: + data = color.MagentaString(e.EventType.String()) + + case s.EventTypeChildWorkflowExecutionTimedOut: + data = color.YellowString(e.EventType.String()) + + case s.EventTypeChildWorkflowExecutionTerminated: + data = e.EventType.String() + + case s.EventTypeSignalExternalWorkflowExecutionInitiated: + data = e.EventType.String() + + case s.EventTypeSignalExternalWorkflowExecutionFailed: + data = color.RedString(e.EventType.String()) + + case s.EventTypeExternalWorkflowExecutionSignaled: + data = e.EventType.String() + + default: + data = e.EventType.String() + } + return data +} diff --git a/tools/cli/workflow.go b/tools/cli/workflow.go index c6ab9e3fd11..11d354d41c4 100644 --- a/tools/cli/workflow.go +++ b/tools/cli/workflow.go @@ -36,6 +36,10 @@ func newWorkflowCommands() []cli.Command { Name: FlagRunIDWithAlias, Usage: "RunID", }, + cli.BoolFlag{ + Name: FlagPrintDateTimeWithAlias, + Usage: "Print time stamp", + }, cli.BoolFlag{ Name: FlagPrintRawTimeWithAlias, Usage: "Print raw time stamp", @@ -53,6 +57,20 @@ func newWorkflowCommands() []cli.Command { Name: "showid", Usage: "show workflow history with given workflow_id and optional run_id (a shortcut of `show -w -r `)", Description: "cadence workflow showid . workflow_id is required; run_id is optional", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: FlagPrintDateTimeWithAlias, + Usage: "Print time stamp", + }, + cli.BoolFlag{ + Name: FlagPrintRawTimeWithAlias, + Usage: "Print raw time stamp", + }, + cli.StringFlag{ + Name: FlagOutputFilenameWithAlias, + Usage: "Serialize history event to a file", + }, + }, Action: func(c *cli.Context) { ShowHistoryWithWID(c) }, @@ -210,12 +228,18 @@ func newWorkflowCommands() []cli.Command { }, }, { - Name: "list", - Usage: "list open or closed workflow executions", + Name: "list", + Aliases: []string{"l"}, + Usage: "list open or closed workflow executions", + Description: "list one page (default size 10 items) by default, use flag --pagesize to change page size", Flags: []cli.Flag{ cli.BoolFlag{ Name: FlagOpenWithAlias, - Usage: "list for open workflow executions, default is to list for closed ones", + Usage: "List for open workflow executions, default is to list for closed ones", + }, + cli.BoolFlag{ + Name: FlagMoreWithAlias, + Usage: "List more pages, default is to list one page of default page size 10", }, cli.IntFlag{ Name: FlagPageSizeWithAlias, @@ -242,11 +266,53 @@ func newWorkflowCommands() []cli.Command { Name: FlagPrintRawTimeWithAlias, Usage: "Print raw time stamp", }, + cli.BoolFlag{ + Name: FlagPrintDateTimeWithAlias, + Usage: "Print full date time in '2006-01-02T15:04:05Z07:00' format", + }, }, Action: func(c *cli.Context) { ListWorkflow(c) }, }, + { + Name: "listall", + Aliases: []string{"la"}, + Usage: "list all open or closed workflow executions", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: FlagOpenWithAlias, + Usage: "List for open workflow executions, default is to list for closed ones", + }, + cli.StringFlag{ + Name: FlagEarliestTimeWithAlias, + Usage: "EarliestTime of start time, supported formats are '2006-01-02T15:04:05Z07:00' and raw UnixNano", + }, + cli.StringFlag{ + Name: FlagLatestTimeWithAlias, + Usage: "LatestTime of start time, supported formats are '2006-01-02T15:04:05Z07:00' and raw UnixNano", + }, + cli.StringFlag{ + Name: FlagWorkflowIDWithAlias, + Usage: "WorkflowID", + }, + cli.StringFlag{ + Name: FlagWorkflowTypeWithAlias, + Usage: "WorkflowTypeName", + }, + cli.BoolFlag{ + Name: FlagPrintRawTimeWithAlias, + Usage: "Print raw time stamp", + }, + cli.BoolFlag{ + Name: FlagPrintDateTimeWithAlias, + Usage: "Print full date time in '2006-01-02T15:04:05Z07:00' format", + }, + }, + Action: func(c *cli.Context) { + ListAllWorkflow(c) + }, + }, { Name: "query", Usage: "query workflow execution",