Skip to content

Commit

Permalink
feat(agentctl): Support specifying seq number for history and improve…
Browse files Browse the repository at this point in the history
… layout

Updated layout:

 SEQ     TYPE            START  INPUT      OPERATIONS   RESULT  SUMMARY
    0  ⟱  config replace  50m    17 values  -            ok      Initial resync
    1  ⇩  config change   7m     3  values  11 executed  ok
    2  ⇧  status update   7m     1  values  1  executed  ok
    3  ⇧  status update   7m     1  values  1  executed  ok
    4  ⇅  config check    5m     -          -            ok

Signed-off-by: Ondrej Fabry <ofabry@cisco.com>
  • Loading branch information
ondrej-fabry committed Aug 28, 2020
1 parent 1ffa39f commit 2fea4c9
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 62 deletions.
3 changes: 2 additions & 1 deletion cmd/agentctl/api/types/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ type SchedulerResyncOptions struct {
}

type SchedulerHistoryOptions struct {
Count int
Count int
SeqNum int
}
12 changes: 12 additions & 0 deletions cmd/agentctl/client/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,27 @@ func (c *Client) SchedulerResync(ctx context.Context, opts types.SchedulerResync

func (c *Client) SchedulerHistory(ctx context.Context, opts types.SchedulerHistoryOptions) (api.RecordedTxns, error) {
query := url.Values{}
if opts.SeqNum >= 0 {
query.Set("seq-num", fmt.Sprint(opts.SeqNum))
}

resp, err := c.get(ctx, "/scheduler/txn-history", query, nil)
if err != nil {
return nil, err
}

if opts.SeqNum >= 0 {
var rectxn api.RecordedTxn
if err := json.NewDecoder(resp.body).Decode(&rectxn); err != nil {
return nil, fmt.Errorf("decoding reply failed: %v", err)
}
return api.RecordedTxns{&rectxn}, nil
}

var rectxn api.RecordedTxns
if err := json.NewDecoder(resp.body).Decode(&rectxn); err != nil {
return nil, fmt.Errorf("decoding reply failed: %v", err)
}

return rectxn, nil
}
137 changes: 96 additions & 41 deletions cmd/agentctl/commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"io"
"io/ioutil"
"strconv"
"time"

yaml2 "github.com/ghodss/yaml"
Expand Down Expand Up @@ -194,7 +195,7 @@ func runConfigRetrieve(cli agentcli.Cli, opts ConfigRetrieveOptions) error {
if len(format) == 0 {
format = `yaml`
}
if err := formatAsTemplate(cli.Out(), format, resp); err != nil {
if err := formatAsTemplate(cli.Out(), format, resp.Dump); err != nil {
return err
}

Expand Down Expand Up @@ -256,10 +257,18 @@ func newConfigHistoryCommand(cli agentcli.Cli) *cobra.Command {
opts ConfigHistoryOptions
)
cmd := &cobra.Command{
Use: "history",
Short: "Retrieve config history",
Args: cobra.NoArgs,
Use: "history [REF]",
Short: "Show config history",
Long: `Show history of config changes and status updates
Prints a table of most important information about the history of changes to
config and status updates that have occurred. You can filter the output by
specifying a reference to sequence number (txn ID).`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
opts.TxnRef = args[0]
}
return runConfigHistory(cli, opts)
},
}
Expand All @@ -269,16 +278,25 @@ func newConfigHistoryCommand(cli agentcli.Cli) *cobra.Command {
}

type ConfigHistoryOptions struct {
Format string
Verbose bool
Retry bool
Format string
TxnRef string
}

func runConfigHistory(cli agentcli.Cli, opts ConfigHistoryOptions) error {
func runConfigHistory(cli agentcli.Cli, opts ConfigHistoryOptions) (err error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

txns, err := cli.Client().SchedulerHistory(ctx, types.SchedulerHistoryOptions{})
ref := -1
if opts.TxnRef != "" {
ref, err = strconv.Atoi(opts.TxnRef)
if err != nil {
return fmt.Errorf("invalid reference: %q, use number > 0", opts.TxnRef)
}
}

txns, err := cli.Client().SchedulerHistory(ctx, types.SchedulerHistoryOptions{
SeqNum: ref,
})
if err != nil {
return err
}
Expand All @@ -296,7 +314,7 @@ func runConfigHistory(cli agentcli.Cli, opts ConfigHistoryOptions) error {
func printHistoryTable(out io.Writer, txns kvs.RecordedTxns) {
table := tablewriter.NewWriter(out)
table.SetHeader([]string{
"Seq", "", "Type", "", "Age", "Summary", "Result",
"Seq", "", "Type", "Start", "Input", "Operations", "Result", "Summary",
})
table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
Expand All @@ -310,61 +328,93 @@ func printHistoryTable(out io.Writer, txns kvs.RecordedTxns) {
table.SetTablePadding("\t")

for _, txn := range txns {
typ := kvs.TxnTypeToString(txn.TxnType)
info := txn.Description
if txn.TxnType == kvs.NBTransaction && txn.ResyncType != kvs.NotResync {
info = fmt.Sprintf("%s", kvs.ResyncTypeToString(txn.ResyncType))
}
elapsed := txn.Stop.Sub(txn.Start).Round(time.Millisecond / 10)
took := elapsed.String()
if elapsed < time.Millisecond/10 {
took = "<.1ms"
} else if elapsed > time.Millisecond*100 {
took = elapsed.Round(time.Millisecond).String()
}
_ = took
typ := getTxnType(txn)
clr := getTxnColor(txn)

result := txnErrors(txn)
resClr := tablewriter.FgGreenColor
if result != "" {
resClr = tablewriter.FgHiRedColor
} else {
result = "ok"
}
var typClr int
switch txn.TxnType {
case kvs.NBTransaction:
typClr = tablewriter.FgYellowColor
case kvs.SBNotification:
typClr = tablewriter.FgCyanColor
case kvs.RetryFailedOps:
typClr = tablewriter.FgMagentaColor
}
age := shortHumanDuration(time.Since(txn.Start))
summary := fmt.Sprintf("%d executed", len(txn.Executed))
var input string
if len(txn.Values) > 0 {
input = fmt.Sprintf("%-2d values", len(txn.Values))
} else {
input = "-"
}
var operation string
if len(txn.Executed) > 0 {
operation = fmt.Sprintf("%-2d executed", len(txn.Executed))
} else {
operation = "-"
}
summary := txn.Description
row := []string{
fmt.Sprintf("%3v", txn.SeqNum),
fmt.Sprintf("%v", txnIcon(txn)),
txnIcon(txn),
typ,
info,
fmt.Sprintf("%-3s", age),
//fmt.Sprintf("%-3s (took %v)", age, took),
fmt.Sprintf("values: %2d -> %s", len(txn.Values), summary),
input,
operation,
result,
summary,
}
clrs := []tablewriter.Colors{
{tablewriter.Normal, typClr},
{tablewriter.Bold, typClr + 60},
{tablewriter.Normal, typClr},
{},
{tablewriter.Bold, clr},
{tablewriter.Normal, clr},
{},
{},
{},
{resClr},
{},
}
table.Rich(row, clrs)
}
table.Render()
}

func getTxnColor(txn *kvs.RecordedTxn) int {
var clr int
switch txn.TxnType {
case kvs.NBTransaction:
if txn.ResyncType == kvs.NotResync {
clr = tablewriter.FgYellowColor
} else if txn.ResyncType == kvs.FullResync {
clr = tablewriter.FgHiYellowColor
} else {
clr = tablewriter.FgYellowColor
}
case kvs.SBNotification:
clr = tablewriter.FgCyanColor
case kvs.RetryFailedOps:
clr = tablewriter.FgMagentaColor
}
return clr
}

func getTxnType(txn *kvs.RecordedTxn) string {
switch txn.TxnType {
case kvs.SBNotification:
return "status update"
case kvs.NBTransaction:
if txn.ResyncType == kvs.FullResync {
return "config replace"
} else if txn.ResyncType == kvs.UpstreamResync {
return "config sync"
} else if txn.ResyncType == kvs.DownstreamResync {
return "config check"
}
return "config change"
case kvs.RetryFailedOps:
return fmt.Sprintf("retry #%d for %d", txn.RetryAttempt, txn.RetryForTxn)
}
return "?"
}

func txnErrors(txn *kvs.RecordedTxn) string {
var errs Errors
for _, r := range txn.Executed {
Expand All @@ -388,7 +438,12 @@ func txnIcon(txn *kvs.RecordedTxn) string {
case kvs.SBNotification:
return "⇧"
case kvs.NBTransaction:
return "⟱"
if txn.ResyncType == kvs.NotResync {
return "⇩"
} else if txn.ResyncType == kvs.FullResync {
return "⟱"
}
return "⇅"
case kvs.RetryFailedOps:
return "↻"
}
Expand Down
17 changes: 17 additions & 0 deletions cmd/agentctl/commands/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,20 @@ func epochTmpl(s int64) time.Time {
func agoTmpl(t time.Time) time.Duration {
return time.Since(t).Round(time.Second)
}

func shortHumanDuration(d time.Duration) string {
if seconds := int(d.Seconds()); seconds < -1 {
return fmt.Sprintf("<invalid>")
} else if seconds < 0 {
return fmt.Sprintf("0s")
} else if seconds < 60 {
return fmt.Sprintf("%ds", seconds)
} else if minutes := int(d.Minutes()); minutes < 60 {
return fmt.Sprintf("%dm", minutes)
} else if hours := int(d.Hours()); hours < 24 {
return fmt.Sprintf("%dh", hours)
} else if hours < 24*365 {
return fmt.Sprintf("%dd", hours/24)
}
return fmt.Sprintf("%dy", int(d.Hours()/24/365))
}
22 changes: 2 additions & 20 deletions cmd/agentctl/commands/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,12 @@ package commands

import (
"fmt"
"time"
)

type Colorer interface {
type colored interface {
Code() string
}

func escapeClr(c Colorer, s interface{}) string {
func escapeClr(c colored, s interface{}) string {
return fmt.Sprintf("\xff\x1b[%sm\xff%v\xff\x1b[0m\xff", c.Code(), s)
}

func shortHumanDuration(d time.Duration) string {
if seconds := int(d.Seconds()); seconds < -1 {
return fmt.Sprintf("<invalid>")
} else if seconds < 0 {
return fmt.Sprintf("0s")
} else if seconds < 60 {
return fmt.Sprintf("%ds", seconds)
} else if minutes := int(d.Minutes()); minutes < 60 {
return fmt.Sprintf("%dm", minutes)
} else if hours := int(d.Hours()); hours < 24 {
return fmt.Sprintf("%dh", hours)
} else if hours < 24*365 {
return fmt.Sprintf("%dd", hours/24)
}
return fmt.Sprintf("%dy", int(d.Hours()/24/365))
}

0 comments on commit 2fea4c9

Please sign in to comment.