From 3f0d46c0a34de20b8b20bb15955757f07a398b83 Mon Sep 17 00:00:00 2001 From: Preetha Appan Date: Thu, 26 Oct 2017 09:21:05 -0500 Subject: [PATCH 1/2] Adds modify time to job/alloc status CLI output, and pretty prints --- command/alloc_status.go | 6 +++++- command/helpers.go | 5 +++++ command/job_status.go | 16 ++++++++++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/command/alloc_status.go b/command/alloc_status.go index 2f206f40b245..2c16bcd6f4e7 100644 --- a/command/alloc_status.go +++ b/command/alloc_status.go @@ -214,6 +214,9 @@ func (c *AllocStatusCommand) Run(args []string) int { } func formatAllocBasicInfo(alloc *api.Allocation, client *api.Client, uuidLength int, verbose bool) (string, error) { + formattedCreateTime := formatTimePretty(time.Unix(0, alloc.CreateTime), time.Now()) + formattedModifyTime := formatTimePretty(time.Unix(0, alloc.ModifyTime), time.Now()) + basic := []string{ fmt.Sprintf("ID|%s", limit(alloc.ID, uuidLength)), fmt.Sprintf("Eval ID|%s", limit(alloc.EvalID, uuidLength)), @@ -225,7 +228,8 @@ func formatAllocBasicInfo(alloc *api.Allocation, client *api.Client, uuidLength fmt.Sprintf("Client Description|%s", alloc.ClientDescription), fmt.Sprintf("Desired Status|%s", alloc.DesiredStatus), fmt.Sprintf("Desired Description|%s", alloc.DesiredDescription), - fmt.Sprintf("Created At|%s", formatUnixNanoTime(alloc.CreateTime)), + fmt.Sprintf("Created|%s", formattedCreateTime), + fmt.Sprintf("Modified|%s", formattedModifyTime), } if alloc.DeploymentID != "" { diff --git a/command/helpers.go b/command/helpers.go index 13f74ba9d982..dd58875e9592 100644 --- a/command/helpers.go +++ b/command/helpers.go @@ -75,6 +75,11 @@ func formatTimeDifference(first, second time.Time, d time.Duration) string { return second.Truncate(d).Sub(first.Truncate(d)).String() } +// formatTimePretty rounds off time difference to the nearest second for nicer display +func formatTimePretty(first, second time.Time) string { + return formatTimeDifference(first.Round(time.Second), second.Round(time.Second), time.Second) + " ago" +} + // getLocalNodeID returns the node ID of the local Nomad Client and an error if // it couldn't be determined or the Agent is not running in Client mode. func getLocalNodeID(client *api.Client) (string, error) { diff --git a/command/job_status.go b/command/job_status.go index 0adc69ef39ac..fabe5913bd33 100644 --- a/command/job_status.go +++ b/command/job_status.go @@ -406,9 +406,9 @@ func formatAllocListStubs(stubs []*api.AllocationListStub, verbose bool, uuidLen allocs := make([]string, len(stubs)+1) if verbose { - allocs[0] = "ID|Eval ID|Node ID|Task Group|Version|Desired|Status|Created At" + allocs[0] = "ID|Eval ID|Node ID|Task Group|Version|Desired|Status|Created At|Modified At" for i, alloc := range stubs { - allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%d|%s|%s|%s", + allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%d|%s|%s|%s|%s", limit(alloc.ID, uuidLength), limit(alloc.EvalID, uuidLength), limit(alloc.NodeID, uuidLength), @@ -416,19 +416,23 @@ func formatAllocListStubs(stubs []*api.AllocationListStub, verbose bool, uuidLen alloc.JobVersion, alloc.DesiredStatus, alloc.ClientStatus, - formatUnixNanoTime(alloc.CreateTime)) + formatUnixNanoTime(alloc.CreateTime), + formatUnixNanoTime(alloc.ModifyTime)) } } else { - allocs[0] = "ID|Node ID|Task Group|Version|Desired|Status|Created At" + allocs[0] = "ID|Node ID|Task Group|Version|Desired|Status|Created|Modified" for i, alloc := range stubs { - allocs[i+1] = fmt.Sprintf("%s|%s|%s|%d|%s|%s|%s", + createTimePretty := formatTimePretty(time.Unix(0, alloc.CreateTime), time.Now()) + modTimePretty := formatTimePretty(time.Unix(0, alloc.ModifyTime), time.Now()) + allocs[i+1] = fmt.Sprintf("%s|%s|%s|%d|%s|%s|%s|%s", limit(alloc.ID, uuidLength), limit(alloc.NodeID, uuidLength), alloc.TaskGroup, alloc.JobVersion, alloc.DesiredStatus, alloc.ClientStatus, - formatUnixNanoTime(alloc.CreateTime)) + createTimePretty, + modTimePretty) } } From f887cf5fd326f476df4e6d8658b4c2bdf0ef472f Mon Sep 17 00:00:00 2001 From: Preetha Appan Date: Fri, 27 Oct 2017 17:24:42 -0500 Subject: [PATCH 2/2] New pretty printer that imitates the short form from time.Duration.String() with approximated values for days and beyond. Used in cli output for allocation create/modify times --- command/alloc_status.go | 11 +++- command/alloc_status_test.go | 17 ++++-- command/helpers.go | 111 ++++++++++++++++++++++++++++++++++- command/helpers_test.go | 32 ++++++++++ command/job_status.go | 6 +- command/job_status_test.go | 13 +++- 6 files changed, 175 insertions(+), 15 deletions(-) diff --git a/command/alloc_status.go b/command/alloc_status.go index 2c16bcd6f4e7..d1e75932e311 100644 --- a/command/alloc_status.go +++ b/command/alloc_status.go @@ -214,8 +214,15 @@ func (c *AllocStatusCommand) Run(args []string) int { } func formatAllocBasicInfo(alloc *api.Allocation, client *api.Client, uuidLength int, verbose bool) (string, error) { - formattedCreateTime := formatTimePretty(time.Unix(0, alloc.CreateTime), time.Now()) - formattedModifyTime := formatTimePretty(time.Unix(0, alloc.ModifyTime), time.Now()) + var formattedCreateTime, formattedModifyTime string + + if verbose { + formattedCreateTime = formatUnixNanoTime(alloc.CreateTime) + formattedModifyTime = formatUnixNanoTime(alloc.ModifyTime) + } else { + formattedCreateTime = prettyTimeDiff(time.Unix(0, alloc.CreateTime), time.Now()) + formattedModifyTime = prettyTimeDiff(time.Unix(0, alloc.ModifyTime), time.Now()) + } basic := []string{ fmt.Sprintf("ID|%s", limit(alloc.ID, uuidLength)), diff --git a/command/alloc_status_test.go b/command/alloc_status_test.go index f858eaf6b1dc..9be04f3dc458 100644 --- a/command/alloc_status_test.go +++ b/command/alloc_status_test.go @@ -128,9 +128,14 @@ func TestAllocStatusCommand_Run(t *testing.T) { t.Fatalf("expected exit 0, got: %d", code) } out := ui.OutputWriter.String() - if !strings.Contains(out, "Created At") { - t.Fatalf("expected to have 'Created At' but saw: %s", out) + if !strings.Contains(out, "Created") { + t.Fatalf("expected to have 'Created' but saw: %s", out) } + + if !strings.Contains(out, "Modified") { + t.Fatalf("expected to have 'Modified' but saw: %s", out) + } + ui.OutputWriter.Reset() if code := cmd.Run([]string{"-address=" + url, "-verbose", allocId1}); code != 0 { @@ -140,8 +145,8 @@ func TestAllocStatusCommand_Run(t *testing.T) { if !strings.Contains(out, allocId1) { t.Fatal("expected to find alloc id in output") } - if !strings.Contains(out, "Created At") { - t.Fatalf("expected to have 'Created At' but saw: %s", out) + if !strings.Contains(out, "Created") { + t.Fatalf("expected to have 'Created' but saw: %s", out) } ui.OutputWriter.Reset() @@ -150,8 +155,8 @@ func TestAllocStatusCommand_Run(t *testing.T) { t.Fatalf("expected exit 0, got: %d", code) } out = ui.OutputWriter.String() - if !strings.Contains(out, "Created At") { - t.Fatalf("expected to have 'Created At' but saw: %s", out) + if !strings.Contains(out, "Created") { + t.Fatalf("expected to have 'Created' but saw: %s", out) } ui.OutputWriter.Reset() diff --git a/command/helpers.go b/command/helpers.go index dd58875e9592..bf2f9dbbb49d 100644 --- a/command/helpers.go +++ b/command/helpers.go @@ -75,9 +75,114 @@ func formatTimeDifference(first, second time.Time, d time.Duration) string { return second.Truncate(d).Sub(first.Truncate(d)).String() } -// formatTimePretty rounds off time difference to the nearest second for nicer display -func formatTimePretty(first, second time.Time) string { - return formatTimeDifference(first.Round(time.Second), second.Round(time.Second), time.Second) + " ago" +// fmtInt formats v into the tail of buf. +// It returns the index where the output begins. +func fmtInt(buf []byte, v uint64) int { + w := len(buf) + for v > 0 { + w-- + buf[w] = byte(v%10) + '0' + v /= 10 + } + return w +} + +// prettyTimeDiff prints a human readable time difference. +// It uses abbreviated forms for each period - s for seconds, m for minutes, h for hours, +// d for days, mo for months, and y for years. Time difference is rounded to the nearest second, +// and the top two least granular periods are returned. For example, if the time difference +// is 10 months, 12 days, 3 hours and 2 seconds, the string "10mo12d" is returned. Zero values return the empty string +func prettyTimeDiff(first, second time.Time) string { + // handle zero values + if first.Second() == 0 { + return "" + } + // round to the nearest second + first = first.Round(time.Second) + second = second.Round(time.Second) + + // calculate time difference in seconds + d := second.Sub(first) + u := uint64(d.Seconds()) + + var buf [32]byte + w := len(buf) + secs := u % 60 + + // track indexes of various periods + var indexes []int + + if secs > 0 { + w-- + buf[w] = 's' + // u is now seconds + w = fmtInt(buf[:w], secs) + indexes = append(indexes, w) + } + u /= 60 + // u is now minutes + if u > 0 { + mins := u % 60 + if mins > 0 { + w-- + buf[w] = 'm' + w = fmtInt(buf[:w], mins) + indexes = append(indexes, w) + } + u /= 60 + // u is now hours + if u > 0 { + hrs := u % 24 + if hrs > 0 { + w-- + buf[w] = 'h' + w = fmtInt(buf[:w], hrs) + indexes = append(indexes, w) + } + u /= 24 + } + // u is now days + if u > 0 { + days := u % 30 + if days > 0 { + w-- + buf[w] = 'd' + w = fmtInt(buf[:w], days) + indexes = append(indexes, w) + } + u /= 30 + } + // u is now months + if u > 0 { + months := u % 12 + if months > 0 { + w-- + buf[w] = 'o' + w-- + buf[w] = 'm' + w = fmtInt(buf[:w], months) + indexes = append(indexes, w) + } + u /= 12 + } + // u is now years + if u > 0 { + w-- + buf[w] = 'y' + w = fmtInt(buf[:w], u) + indexes = append(indexes, w) + } + } + start := w + end := len(buf) + + // truncate to the first two periods + num_periods := len(indexes) + if num_periods > 2 { + end = indexes[num_periods-3] + } + return string(buf[start:end]) + " ago" + } // getLocalNodeID returns the node ID of the local Nomad Client and an error if diff --git a/command/helpers_test.go b/command/helpers_test.go index a0a538c8bfa3..617a732f37f0 100644 --- a/command/helpers_test.go +++ b/command/helpers_test.go @@ -294,3 +294,35 @@ func TestJobGetter_HTTPServer(t *testing.T) { t.Fatalf("Unexpected file") } } + +func TestPrettyTimeDiff(t *testing.T) { + test_cases := []struct { + d time.Duration + exp string + }{ + {-740 * time.Second, "12m20s ago"}, + {-12 * time.Minute, "12m ago"}, + {-60 * time.Minute, "1h ago"}, + {-80 * time.Minute, "1h20m ago"}, + {-6 * time.Hour, "6h ago"}, + {-22165 * time.Second, "6h9m ago"}, + {-100 * time.Hour, "4d4h ago"}, + {-438000 * time.Minute, "10mo4d ago"}, + {-20460 * time.Hour, "2y4mo ago"}, + } + for _, tc := range test_cases { + t2 := time.Now().Add(tc.d) + out := prettyTimeDiff(t2, time.Now()) + if out != tc.exp { + t.Fatalf("expected :%v but got :%v", tc.exp, out) + } + } + + var t1 time.Time + out := prettyTimeDiff(t1, time.Now()) + + if out != "" { + t.Fatalf("Expected empty output but got:%v", out) + } + +} diff --git a/command/job_status.go b/command/job_status.go index fabe5913bd33..bf239ff936e7 100644 --- a/command/job_status.go +++ b/command/job_status.go @@ -406,7 +406,7 @@ func formatAllocListStubs(stubs []*api.AllocationListStub, verbose bool, uuidLen allocs := make([]string, len(stubs)+1) if verbose { - allocs[0] = "ID|Eval ID|Node ID|Task Group|Version|Desired|Status|Created At|Modified At" + allocs[0] = "ID|Eval ID|Node ID|Task Group|Version|Desired|Status|Created|Modified" for i, alloc := range stubs { allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%d|%s|%s|%s|%s", limit(alloc.ID, uuidLength), @@ -422,8 +422,8 @@ func formatAllocListStubs(stubs []*api.AllocationListStub, verbose bool, uuidLen } else { allocs[0] = "ID|Node ID|Task Group|Version|Desired|Status|Created|Modified" for i, alloc := range stubs { - createTimePretty := formatTimePretty(time.Unix(0, alloc.CreateTime), time.Now()) - modTimePretty := formatTimePretty(time.Unix(0, alloc.ModifyTime), time.Now()) + createTimePretty := prettyTimeDiff(time.Unix(0, alloc.CreateTime), time.Now()) + modTimePretty := prettyTimeDiff(time.Unix(0, alloc.ModifyTime), time.Now()) allocs[i+1] = fmt.Sprintf("%s|%s|%s|%d|%s|%s|%s|%s", limit(alloc.ID, uuidLength), limit(alloc.NodeID, uuidLength), diff --git a/command/job_status_test.go b/command/job_status_test.go index 5a7c71ce86dc..4a07ea71fc0e 100644 --- a/command/job_status_test.go +++ b/command/job_status_test.go @@ -113,9 +113,12 @@ func TestJobStatusCommand_Run(t *testing.T) { if !strings.Contains(out, "Allocations") { t.Fatalf("should dump allocations") } - if !strings.Contains(out, "Created At") { + if !strings.Contains(out, "Created") { t.Fatal("should have created header") } + if !strings.Contains(out, "Modified") { + t.Fatal("should have modified header") + } ui.ErrorWriter.Reset() ui.OutputWriter.Reset() @@ -138,6 +141,14 @@ func TestJobStatusCommand_Run(t *testing.T) { if !strings.Contains(out, "job1_sfx") || strings.Contains(out, "job2_sfx") { t.Fatalf("expected only job1_sfx, got: %s", out) } + + if !strings.Contains(out, "Created") { + t.Fatal("should have created header") + } + + if !strings.Contains(out, "Modified") { + t.Fatal("should have modified header") + } ui.OutputWriter.Reset() // Query in short view mode