print stats after backup (#3128)
Prints backup stats to the CLI following completion. In case of multiple users, the stats for each backup is printed at the end of the backup, rather than at the end of the command. --- #### Does this PR need a docs update or release note? - [x] ✅ Yes, it's included #### Type of change - [x] 🌻 Feature #### Test Plan - [x] 💪 Manual - [x] ⚡ Unit test
This commit is contained in:
parent
d5fac8a480
commit
6e982d6bdc
@ -230,8 +230,14 @@ func runBackups(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bIDs = append(bIDs, string(bo.Results.BackupID))
|
bIDs = append(bIDs, string(bo.Results.BackupID))
|
||||||
|
|
||||||
|
if !DisplayJSONFormat() {
|
||||||
|
Infof(ctx, "Done\n")
|
||||||
|
printBackupStats(ctx, r, string(bo.Results.BackupID))
|
||||||
|
} else {
|
||||||
Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID)
|
Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bups, berrs := r.Backups(ctx, bIDs)
|
bups, berrs := r.Backups(ctx, bIDs)
|
||||||
if berrs.Failure() != nil {
|
if berrs.Failure() != nil {
|
||||||
@ -335,3 +341,13 @@ func getAccountAndConnect(ctx context.Context) (repository.Repository, *account.
|
|||||||
func ifShow(flag string) bool {
|
func ifShow(flag string) bool {
|
||||||
return strings.ToLower(strings.TrimSpace(flag)) == "show"
|
return strings.ToLower(strings.TrimSpace(flag)) == "show"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printBackupStats(ctx context.Context, r repository.Repository, bid string) {
|
||||||
|
b, err := r.Backup(ctx, bid)
|
||||||
|
if err != nil {
|
||||||
|
logger.CtxErr(ctx, err).Error("finding backup immediately after backup operation completion")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ToPrintable().Stats.Print(ctx)
|
||||||
|
Info(ctx, " ")
|
||||||
|
}
|
||||||
|
|||||||
@ -50,8 +50,8 @@ func AddOutputFlag(cmd *cobra.Command) {
|
|||||||
cobra.CheckErr(fs.MarkHidden("json-debug"))
|
cobra.CheckErr(fs.MarkHidden("json-debug"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONFormat returns true if the printer plans to output as json.
|
// DisplayJSONFormat returns true if the printer plans to output as json.
|
||||||
func JSONFormat() bool {
|
func DisplayJSONFormat() bool {
|
||||||
return outputAsJSON || outputAsJSONDebug
|
return outputAsJSON || outputAsJSONDebug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,12 @@ package backup
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/cli/print"
|
"github.com/alcionai/corso/src/cli/print"
|
||||||
"github.com/alcionai/corso/src/internal/common"
|
"github.com/alcionai/corso/src/internal/common"
|
||||||
"github.com/alcionai/corso/src/internal/model"
|
"github.com/alcionai/corso/src/internal/model"
|
||||||
@ -141,6 +144,8 @@ func New(
|
|||||||
// CLI Output
|
// CLI Output
|
||||||
// --------------------------------------------------------------------------------
|
// --------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ----- print backups
|
||||||
|
|
||||||
// Print writes the Backup to StdOut, in the format requested by the caller.
|
// Print writes the Backup to StdOut, in the format requested by the caller.
|
||||||
func (b Backup) Print(ctx context.Context) {
|
func (b Backup) Print(ctx context.Context) {
|
||||||
print.Item(ctx, b)
|
print.Item(ctx, b)
|
||||||
@ -163,35 +168,35 @@ func PrintAll(ctx context.Context, bs []*Backup) {
|
|||||||
|
|
||||||
type Printable struct {
|
type Printable struct {
|
||||||
ID model.StableID `json:"id"`
|
ID model.StableID `json:"id"`
|
||||||
ErrorCount int `json:"errorCount"`
|
|
||||||
StartedAt time.Time `json:"started at"`
|
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
BytesRead int64 `json:"bytesRead"`
|
|
||||||
BytesUploaded int64 `json:"bytesUploaded"`
|
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner"`
|
||||||
|
Stats backupStats `json:"stats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToPrintable reduces the Backup to its minimally printable details.
|
||||||
|
func (b Backup) ToPrintable() Printable {
|
||||||
|
return Printable{
|
||||||
|
ID: b.ID,
|
||||||
|
Status: b.Status,
|
||||||
|
Version: "0",
|
||||||
|
Owner: b.Selector.DiscreteOwner,
|
||||||
|
Stats: b.toStats(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MinimumPrintable reduces the Backup to its minimally printable details.
|
// MinimumPrintable reduces the Backup to its minimally printable details.
|
||||||
func (b Backup) MinimumPrintable() any {
|
func (b Backup) MinimumPrintable() any {
|
||||||
return Printable{
|
return b.ToPrintable()
|
||||||
ID: b.ID,
|
|
||||||
ErrorCount: b.ErrorCount,
|
|
||||||
StartedAt: b.StartedAt,
|
|
||||||
Status: b.Status,
|
|
||||||
Version: "0",
|
|
||||||
BytesRead: b.BytesRead,
|
|
||||||
BytesUploaded: b.BytesUploaded,
|
|
||||||
Owner: b.Selector.DiscreteOwner,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headers returns the human-readable names of properties in a Backup
|
// Headers returns the human-readable names of properties in a Backup
|
||||||
// for printing out to a terminal in a columnar display.
|
// for printing out to a terminal in a columnar display.
|
||||||
func (b Backup) Headers() []string {
|
func (b Backup) Headers() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"Started At",
|
|
||||||
"ID",
|
"ID",
|
||||||
|
"Started At",
|
||||||
|
"Duration",
|
||||||
"Status",
|
"Status",
|
||||||
"Resource Owner",
|
"Resource Owner",
|
||||||
}
|
}
|
||||||
@ -255,10 +260,78 @@ func (b Backup) Values() []string {
|
|||||||
name = b.Selector.DiscreteOwner
|
name = b.Selector.DiscreteOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bs := b.toStats()
|
||||||
|
|
||||||
return []string{
|
return []string{
|
||||||
common.FormatTabularDisplayTime(b.StartedAt),
|
|
||||||
string(b.ID),
|
string(b.ID),
|
||||||
|
common.FormatTabularDisplayTime(b.StartedAt),
|
||||||
|
bs.EndedAt.Sub(bs.StartedAt).String(),
|
||||||
status,
|
status,
|
||||||
name,
|
name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- print backup stats
|
||||||
|
|
||||||
|
func (b Backup) toStats() backupStats {
|
||||||
|
return backupStats{
|
||||||
|
ID: string(b.ID),
|
||||||
|
BytesRead: b.BytesRead,
|
||||||
|
BytesUploaded: b.BytesUploaded,
|
||||||
|
EndedAt: b.CompletedAt,
|
||||||
|
ErrorCount: b.ErrorCount,
|
||||||
|
ItemsRead: b.ItemsRead,
|
||||||
|
ItemsSkipped: b.TotalSkippedItems,
|
||||||
|
ItemsWritten: b.ItemsWritten,
|
||||||
|
StartedAt: b.StartedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface compliance checks
|
||||||
|
var _ print.Printable = &backupStats{}
|
||||||
|
|
||||||
|
type backupStats struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
BytesRead int64 `json:"bytesRead"`
|
||||||
|
BytesUploaded int64 `json:"bytesUploaded"`
|
||||||
|
EndedAt time.Time `json:"endedAt"`
|
||||||
|
ErrorCount int `json:"errorCount"`
|
||||||
|
ItemsRead int `json:"itemsRead"`
|
||||||
|
ItemsSkipped int `json:"itemsSkipped"`
|
||||||
|
ItemsWritten int `json:"itemsWritten"`
|
||||||
|
StartedAt time.Time `json:"startedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print writes the Backup to StdOut, in the format requested by the caller.
|
||||||
|
func (bs backupStats) Print(ctx context.Context) {
|
||||||
|
print.Item(ctx, bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinimumPrintable reduces the Backup to its minimally printable details.
|
||||||
|
func (bs backupStats) MinimumPrintable() any {
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers returns the human-readable names of properties in a Backup
|
||||||
|
// for printing out to a terminal in a columnar display.
|
||||||
|
func (bs backupStats) Headers() []string {
|
||||||
|
return []string{
|
||||||
|
"ID",
|
||||||
|
"Bytes Uploaded",
|
||||||
|
"Items Uploaded",
|
||||||
|
"Items Skipped",
|
||||||
|
"Errors",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the values matching the Headers list for printing
|
||||||
|
// out to a terminal in a columnar display.
|
||||||
|
func (bs backupStats) Values() []string {
|
||||||
|
return []string{
|
||||||
|
bs.ID,
|
||||||
|
humanize.Bytes(uint64(bs.BytesUploaded)),
|
||||||
|
strconv.Itoa(bs.ItemsWritten),
|
||||||
|
strconv.Itoa(bs.ItemsSkipped),
|
||||||
|
strconv.Itoa(bs.ErrorCount),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package backup_test
|
package backup_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@ -50,7 +52,7 @@ func stubBackup(t time.Time, ownerID, ownerName string) backup.Backup {
|
|||||||
},
|
},
|
||||||
StartAndEndTime: stats.StartAndEndTime{
|
StartAndEndTime: stats.StartAndEndTime{
|
||||||
StartedAt: t,
|
StartedAt: t,
|
||||||
CompletedAt: t,
|
CompletedAt: t.Add(1 * time.Minute),
|
||||||
},
|
},
|
||||||
SkippedCounts: stats.SkippedCounts{
|
SkippedCounts: stats.SkippedCounts{
|
||||||
TotalSkippedItems: 1,
|
TotalSkippedItems: 1,
|
||||||
@ -63,22 +65,27 @@ func (suite *BackupUnitSuite) TestBackup_HeadersValues() {
|
|||||||
var (
|
var (
|
||||||
t = suite.T()
|
t = suite.T()
|
||||||
now = time.Now()
|
now = time.Now()
|
||||||
|
later = now.Add(1 * time.Minute)
|
||||||
b = stubBackup(now, "id", "name")
|
b = stubBackup(now, "id", "name")
|
||||||
expectHs = []string{
|
expectHs = []string{
|
||||||
"Started At",
|
|
||||||
"ID",
|
"ID",
|
||||||
|
"Started At",
|
||||||
|
"Duration",
|
||||||
"Status",
|
"Status",
|
||||||
"Resource Owner",
|
"Resource Owner",
|
||||||
}
|
}
|
||||||
nowFmt = common.FormatTabularDisplayTime(now)
|
nowFmt = common.FormatTabularDisplayTime(now)
|
||||||
expectVs = []string{
|
expectVs = []string{
|
||||||
nowFmt,
|
|
||||||
"id",
|
"id",
|
||||||
|
nowFmt,
|
||||||
|
"1m0s",
|
||||||
"status (2 errors, 1 skipped: 1 malware)",
|
"status (2 errors, 1 skipped: 1 malware)",
|
||||||
"test",
|
"test",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
b.StartAndEndTime.CompletedAt = later
|
||||||
|
|
||||||
// single skipped malware
|
// single skipped malware
|
||||||
hs := b.Headers()
|
hs := b.Headers()
|
||||||
assert.Equal(t, expectHs, hs)
|
assert.Equal(t, expectHs, hs)
|
||||||
@ -182,7 +189,7 @@ func (suite *BackupUnitSuite) TestBackup_Values_statusVariations() {
|
|||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
result := test.bup.Values()
|
result := test.bup.Values()
|
||||||
assert.Equal(suite.T(), test.expect, result[2], "status value")
|
assert.Equal(suite.T(), test.expect, result[3], "status value")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,10 +204,57 @@ func (suite *BackupUnitSuite) TestBackup_MinimumPrintable() {
|
|||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
assert.Equal(t, b.ID, result.ID, "id")
|
assert.Equal(t, b.ID, result.ID, "id")
|
||||||
assert.Equal(t, 2, result.ErrorCount, "error count")
|
assert.Equal(t, 2, result.Stats.ErrorCount, "error count")
|
||||||
assert.Equal(t, now, result.StartedAt, "started at")
|
assert.Equal(t, now, result.Stats.StartedAt, "started at")
|
||||||
assert.Equal(t, b.Status, result.Status, "status")
|
assert.Equal(t, b.Status, result.Status, "status")
|
||||||
assert.Equal(t, b.BytesRead, result.BytesRead, "size")
|
assert.Equal(t, b.BytesRead, result.Stats.BytesRead, "size")
|
||||||
assert.Equal(t, b.BytesUploaded, result.BytesUploaded, "stored size")
|
assert.Equal(t, b.BytesUploaded, result.Stats.BytesUploaded, "stored size")
|
||||||
assert.Equal(t, b.Selector.DiscreteOwner, result.Owner, "owner")
|
assert.Equal(t, b.Selector.DiscreteOwner, result.Owner, "owner")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *BackupUnitSuite) TestStats() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
start = time.Now()
|
||||||
|
b = stubBackup(start, "owner", "ownername")
|
||||||
|
s = b.ToPrintable().Stats
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, b.BytesRead, s.BytesRead, "bytes read")
|
||||||
|
assert.Equal(t, b.BytesUploaded, s.BytesUploaded, "bytes uploaded")
|
||||||
|
assert.Equal(t, b.CompletedAt, s.EndedAt, "completion time")
|
||||||
|
assert.Equal(t, b.ErrorCount, s.ErrorCount, "error count")
|
||||||
|
assert.Equal(t, b.ItemsRead, s.ItemsRead, "items read")
|
||||||
|
assert.Equal(t, b.TotalSkippedItems, s.ItemsSkipped, "items skipped")
|
||||||
|
assert.Equal(t, b.ItemsWritten, s.ItemsWritten, "items written")
|
||||||
|
assert.Equal(t, b.StartedAt, s.StartedAt, "started at")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BackupUnitSuite) TestStats_headersValues() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
start = time.Now()
|
||||||
|
b = stubBackup(start, "owner", "ownername")
|
||||||
|
s = b.ToPrintable().Stats
|
||||||
|
)
|
||||||
|
|
||||||
|
expectHeaders := []string{
|
||||||
|
"ID",
|
||||||
|
"Bytes Uploaded",
|
||||||
|
"Items Uploaded",
|
||||||
|
"Items Skipped",
|
||||||
|
"Errors",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expectHeaders, s.Headers())
|
||||||
|
|
||||||
|
expectValues := []string{
|
||||||
|
"id",
|
||||||
|
humanize.Bytes(uint64(b.BytesUploaded)),
|
||||||
|
strconv.Itoa(b.ItemsWritten),
|
||||||
|
strconv.Itoa(b.TotalSkippedItems),
|
||||||
|
strconv.Itoa(b.ErrorCount),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expectValues, s.Values())
|
||||||
|
}
|
||||||
|
|||||||
@ -139,7 +139,7 @@ type DetailsModel struct {
|
|||||||
// Print writes the DetailModel Entries to StdOut, in the format
|
// Print writes the DetailModel Entries to StdOut, in the format
|
||||||
// requested by the caller.
|
// requested by the caller.
|
||||||
func (dm DetailsModel) PrintEntries(ctx context.Context) {
|
func (dm DetailsModel) PrintEntries(ctx context.Context) {
|
||||||
if print.JSONFormat() {
|
if print.DisplayJSONFormat() {
|
||||||
printJSON(ctx, dm)
|
printJSON(ctx, dm)
|
||||||
} else {
|
} else {
|
||||||
printTable(ctx, dm)
|
printTable(ctx, dm)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user