diff --git a/src/cli/backup/backup.go b/src/cli/backup/backup.go index 73ab32b4e..29631c130 100644 --- a/src/cli/backup/backup.go +++ b/src/cli/backup/backup.go @@ -2,6 +2,7 @@ package backup import ( "context" + "encoding/json" "fmt" "strings" @@ -12,8 +13,10 @@ import ( "github.com/alcionai/corso/src/cli/flags" . "github.com/alcionai/corso/src/cli/print" "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/common/color" "github.com/alcionai/corso/src/internal/common/idname" "github.com/alcionai/corso/src/internal/data" + "github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/pkg/backup" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/control" @@ -187,8 +190,15 @@ func genericCreateCommand( bo, err := r.NewBackupWithLookup(ictx, discSel, ins) if err != nil { - errs = append(errs, clues.WrapWC(ictx, err, owner)) - Errf(ictx, "%v\n", err) + cerr := clues.WrapWC(ictx, err, owner) + errs = append(errs, cerr) + + meta, err := json.Marshal(cerr.Core().Values) + if err != nil { + meta = []byte("Unable to marshal error metadata") + } + + Errf(ictx, "%s\nMessage: %v\nMetadata:%s", "Unable to complete backup", err, meta) continue } @@ -208,8 +218,15 @@ func genericCreateCommand( continue } - errs = append(errs, clues.WrapWC(ictx, err, owner)) - Errf(ictx, "%v\n", err) + cerr := clues.WrapWC(ictx, err, owner) + errs = append(errs, cerr) + + meta, err := json.Marshal(cerr.Core().Values) + if err != nil { + meta = []byte("Unable to marshal error metadata") + } + + Errf(ictx, "%s\nMessage: %v\nMetadata:%s", "Unable to complete backup", err, meta) continue } @@ -217,10 +234,10 @@ func genericCreateCommand( bIDs = append(bIDs, string(bo.Results.BackupID)) if !DisplayJSONFormat() { - Infof(ctx, "Done\n") + Infof(ctx, fmt.Sprintf("Backup complete %s %s", observe.Bullet, color.BlueOutput(bo.Results.BackupID))) printBackupStats(ctx, r, string(bo.Results.BackupID)) } else { - Infof(ctx, "Done - ID: %v\n", bo.Results.BackupID) + Infof(ctx, "Backup complete - ID: %v\n", bo.Results.BackupID) } } @@ -231,10 +248,9 @@ func genericCreateCommand( if len(bups) > 0 { Info(ctx, "Completed Backups:") + backup.PrintAll(ctx, bups) } - backup.PrintAll(ctx, bups) - if len(errs) > 0 { sb := fmt.Sprintf("%d of %d backups failed:\n", len(errs), len(selectorSet)) diff --git a/src/cli/cli.go b/src/cli/cli.go index 3ca7f0cbb..0f41e69a7 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -73,7 +73,7 @@ func preRun(cc *cobra.Command, args []string) error { func handleMailBoxFlag(ctx context.Context, c *cobra.Command, flagNames []string) { if !slices.Contains(flagNames, "user") && !slices.Contains(flagNames, "mailbox") { - print.Errf(ctx, "either --user or --mailbox flag is required") + print.Err(ctx, "either --user or --mailbox flag is required") os.Exit(1) } diff --git a/src/cli/export/export.go b/src/cli/export/export.go index 774cb2f9d..99200c1f5 100644 --- a/src/cli/export/export.go +++ b/src/cli/export/export.go @@ -107,7 +107,7 @@ func runExport( // It would be better to give a progressbar than a spinner, but we // have any way of knowing how many files are available as of now. - diskWriteComplete := observe.MessageWithCompletion(ctx, "Writing data to disk") + diskWriteComplete := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Writing data to disk") err = export.ConsumeExportCollections(ctx, exportLocation, expColl, eo.Errors) diff --git a/src/cli/print/print.go b/src/cli/print/print.go index dfe8a4806..b17b57551 100644 --- a/src/cli/print/print.go +++ b/src/cli/print/print.go @@ -10,6 +10,7 @@ import ( "github.com/tidwall/pretty" "github.com/tomlazar/table" + "github.com/alcionai/corso/src/internal/common/color" "github.com/alcionai/corso/src/internal/observe" ) @@ -83,16 +84,21 @@ func Only(ctx context.Context, e error) error { // Err prints the params to cobra's error writer (stdErr by default) // if s is nil, prints nothing. -// Prepends the message with "Error: " func Err(ctx context.Context, s ...any) { - out(ctx, getRootCmd(ctx).ErrOrStderr(), s...) + cw := color.NewColorableWriter(color.Red, getRootCmd(ctx).ErrOrStderr()) + + s = append([]any{"Error:"}, s...) + + out(ctx, cw, s...) } // Errf prints the params to cobra's error writer (stdErr by default) // if s is nil, prints nothing. -// Prepends the message with "Error: " +// You should ideally be using SimpleError or OperationError. func Errf(ctx context.Context, tmpl string, s ...any) { - outf(ctx, getRootCmd(ctx).ErrOrStderr(), "\nError: \n\t"+tmpl+"\n", s...) + cw := color.NewColorableWriter(color.Red, getRootCmd(ctx).ErrOrStderr()) + tmpl = "Error: " + tmpl + outf(ctx, cw, tmpl, s...) } // Out prints the params to cobra's output writer (stdOut by default) diff --git a/src/go.mod b/src/go.mod index 2d9c4c540..af3609cfe 100644 --- a/src/go.mod +++ b/src/go.mod @@ -10,6 +10,7 @@ require ( github.com/armon/go-metrics v0.4.1 github.com/aws/aws-xray-sdk-go v1.8.3 github.com/cenkalti/backoff/v4 v4.2.1 + github.com/fatih/color v1.15.0 github.com/golang-jwt/jwt/v5 v5.1.0 github.com/google/uuid v1.4.0 github.com/h2non/gock v1.2.0 @@ -51,7 +52,6 @@ require ( github.com/aws/aws-sdk-go v1.47.9 // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-test/deep v1.1.0 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect github.com/google/go-cmp v0.5.9 // indirect diff --git a/src/go.sum b/src/go.sum index 739409aea..869056ce0 100644 --- a/src/go.sum +++ b/src/go.sum @@ -119,6 +119,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= diff --git a/src/internal/common/color/color.go b/src/internal/common/color/color.go new file mode 100644 index 000000000..243b93a15 --- /dev/null +++ b/src/internal/common/color/color.go @@ -0,0 +1,36 @@ +package color + +import ( + "io" + + "github.com/fatih/color" +) + +var ( + Red = color.FgRed + Blue = color.FgBlue + Magenta = color.FgMagenta + Cyan = color.FgCyan + Green = color.FgGreen + White = color.FgWhite + + RedOutput = color.New(Red).SprintFunc() + BlueOutput = color.New(Blue).SprintFunc() + MagentaOutput = color.New(Magenta).SprintFunc() + CyanOutput = color.New(Cyan).SprintFunc() + GreenOutput = color.New(Green).SprintFunc() + GreyOutput = color.New(White).SprintFunc() +) + +type colorableWriter struct { + color color.Attribute + writer io.Writer +} + +func NewColorableWriter(clr color.Attribute, writer io.Writer) io.Writer { + return &colorableWriter{clr, writer} +} + +func (cw *colorableWriter) Write(p []byte) (n int, err error) { + return color.New(cw.color).Fprint(cw.writer, string(p)) +} diff --git a/src/internal/kopia/wrapper.go b/src/internal/kopia/wrapper.go index 6d552c4ff..95366dc8e 100644 --- a/src/internal/kopia/wrapper.go +++ b/src/internal/kopia/wrapper.go @@ -651,6 +651,7 @@ func (w Wrapper) RepoMaintenance( if len(params.Owner) == 0 || (params.Owner != currentOwner && opts.Force) { observe.Message( ctx, + observe.ProgressCfg{}, "updating maintenance user@host to ", clues.Hide(currentOwner)) diff --git a/src/internal/m365/collection/drive/collections.go b/src/internal/m365/collection/drive/collections.go index 4286c0a15..6aacaef1a 100644 --- a/src/internal/m365/collection/drive/collections.go +++ b/src/internal/m365/collection/drive/collections.go @@ -3,7 +3,6 @@ package drive import ( "context" "encoding/json" - "fmt" "io" "strings" @@ -18,7 +17,6 @@ import ( "github.com/alcionai/corso/src/internal/m365/collection/drive/metadata" odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts" "github.com/alcionai/corso/src/internal/m365/support" - "github.com/alcionai/corso/src/internal/observe" bupMD "github.com/alcionai/corso/src/pkg/backup/metadata" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/fault" @@ -283,11 +281,6 @@ func (c *Collections) Get( driveTombstones[driveID] = struct{}{} } - progressBar := observe.MessageWithCompletion( - ctx, - observe.Bulletf(path.FilesCategory.HumanString())) - defer close(progressBar) - // Enumerate drives for the specified resourceOwner pager := c.handler.NewDrivePager(c.protectedResource.ID(), nil) @@ -456,8 +449,6 @@ func (c *Collections) Get( } } - observe.Message(ctx, fmt.Sprintf("Discovered %d items to backup", c.NumItems)) - collections := []data.BackupCollection{} // add all the drives we found diff --git a/src/internal/m365/collection/exchange/backup.go b/src/internal/m365/collection/exchange/backup.go index 67b568b56..f1dcc4a72 100644 --- a/src/internal/m365/collection/exchange/backup.go +++ b/src/internal/m365/collection/exchange/backup.go @@ -2,6 +2,7 @@ package exchange import ( "context" + "fmt" "github.com/alcionai/clues" @@ -42,6 +43,8 @@ func CreateCollections( ProtectedResource: bpc.ProtectedResource, TenantID: tenantID, } + collections map[string]data.BackupCollection + err error ) handler, ok := handlers[category] @@ -49,9 +52,15 @@ func CreateCollections( return nil, clues.NewWC(ctx, "unsupported backup category type") } + pcfg := observe.ProgressCfg{ + Indent: 1, + CompletionMessage: func() string { return fmt.Sprintf("(found %d folders)", len(collections)) }, + } foldersComplete := observe.MessageWithCompletion( ctx, - observe.Bulletf("%s", qp.Category.HumanString())) + pcfg, + qp.Category.HumanString()) + defer close(foldersComplete) rootFolder, cc := handler.NewContainerCache(bpc.ProtectedResource.ID()) @@ -60,7 +69,7 @@ func CreateCollections( return nil, clues.Wrap(err, "populating container cache") } - collections, err := populateCollections( + collections, err = populateCollections( ctx, qp, handler, diff --git a/src/internal/m365/collection/site/backup.go b/src/internal/m365/collection/site/backup.go index 8e7a1c2df..17ad650a0 100644 --- a/src/internal/m365/collection/site/backup.go +++ b/src/internal/m365/collection/site/backup.go @@ -2,6 +2,8 @@ package site import ( "context" + "fmt" + stdpath "path" "github.com/alcionai/clues" @@ -10,6 +12,7 @@ import ( "github.com/alcionai/corso/src/internal/m365/collection/drive" betaAPI "github.com/alcionai/corso/src/internal/m365/service/sharepoint/api" "github.com/alcionai/corso/src/internal/m365/support" + "github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/count" @@ -44,6 +47,18 @@ func CollectLibraries( bpc.Options) ) + msg := fmt.Sprintf( + "%s (%s)", + path.LibrariesCategory.HumanString(), + stdpath.Base(bpc.ProtectedResource.Name())) + + pcfg := observe.ProgressCfg{ + Indent: 1, + CompletionMessage: func() string { return fmt.Sprintf("(found %d items)", colls.NumItems) }, + } + progressBar := observe.MessageWithCompletion(ctx, pcfg, msg) + close(progressBar) + odcs, canUsePreviousBackup, err := colls.Get(ctx, bpc.MetadataCollections, ssmb, errs) if err != nil { return nil, false, graph.Wrap(ctx, err, "getting library") diff --git a/src/internal/m365/service/groups/backup.go b/src/internal/m365/service/groups/backup.go index c04805525..11af8e5e9 100644 --- a/src/internal/m365/service/groups/backup.go +++ b/src/internal/m365/service/groups/backup.go @@ -2,6 +2,7 @@ package groups import ( "context" + "fmt" "github.com/alcionai/clues" "github.com/kopia/kopia/repo/manifest" @@ -70,11 +71,6 @@ func ProduceBackupCollections( break } - progressBar := observe.MessageWithCompletion( - ctx, - observe.Bulletf("%s", scope.Category().PathType().HumanString())) - defer close(progressBar) - var dbcs []data.BackupCollection switch scope.Category().PathType() { @@ -134,15 +130,27 @@ func ProduceBackupCollections( dbcs = append(dbcs, cs...) } - case path.ChannelMessagesCategory: + var ( + cs []data.BackupCollection + canUsePreviousBackup bool + err error + ) + + pcfg := observe.ProgressCfg{ + Indent: 1, + // TODO(meain): Use number of messages and not channels + CompletionMessage: func() string { return fmt.Sprintf("(found %d channels)", len(cs)) }, + } + progressBar := observe.MessageWithCompletion(ctx, pcfg, scope.Category().PathType().HumanString()) + if !isTeam { continue } bh := groups.NewChannelBackupHandler(bpc.ProtectedResource.ID(), ac.Channels()) - cs, canUsePreviousBackup, err := groups.CreateCollections( + cs, canUsePreviousBackup, err = groups.CreateCollections( ctx, bpc, bh, @@ -165,6 +173,8 @@ func ProduceBackupCollections( } dbcs = append(dbcs, cs...) + + close(progressBar) } collections = append(collections, dbcs...) diff --git a/src/internal/m365/service/onedrive/backup.go b/src/internal/m365/service/onedrive/backup.go index 39a524d43..8015d7c58 100644 --- a/src/internal/m365/service/onedrive/backup.go +++ b/src/internal/m365/service/onedrive/backup.go @@ -2,6 +2,7 @@ package onedrive import ( "context" + "fmt" "github.com/alcionai/clues" @@ -9,6 +10,7 @@ import ( "github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/m365/collection/drive" "github.com/alcionai/corso/src/internal/m365/support" + "github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/internal/version" "github.com/alcionai/corso/src/pkg/fault" @@ -55,6 +57,14 @@ func ProduceBackupCollections( su, bpc.Options) + pcfg := observe.ProgressCfg{ + Indent: 1, + CompletionMessage: func() string { return fmt.Sprintf("(found %d files)", nc.NumFiles) }, + } + progressBar := observe.MessageWithCompletion(ctx, pcfg, path.FilesCategory.HumanString()) + + defer close(progressBar) + odcs, canUsePreviousBackup, err = nc.Get(ctx, bpc.MetadataCollections, ssmb, errs) if err != nil { el.AddRecoverable(ctx, clues.Stack(err).Label(fault.LabelForceNoBackupCreation)) diff --git a/src/internal/m365/service/sharepoint/backup.go b/src/internal/m365/service/sharepoint/backup.go index 0f9e83b3a..770970874 100644 --- a/src/internal/m365/service/sharepoint/backup.go +++ b/src/internal/m365/service/sharepoint/backup.go @@ -10,7 +10,6 @@ import ( "github.com/alcionai/corso/src/internal/m365/collection/drive" "github.com/alcionai/corso/src/internal/m365/collection/site" "github.com/alcionai/corso/src/internal/m365/support" - "github.com/alcionai/corso/src/internal/observe" "github.com/alcionai/corso/src/internal/operations/inject" "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/count" @@ -52,11 +51,6 @@ func ProduceBackupCollections( break } - progressBar := observe.MessageWithCompletion( - ctx, - observe.Bulletf("%s", scope.Category().PathType().HumanString())) - defer close(progressBar) - var spcs []data.BackupCollection switch scope.Category().PathType() { diff --git a/src/internal/observe/observe.go b/src/internal/observe/observe.go index 4281f2814..f8da6220d 100644 --- a/src/internal/observe/observe.go +++ b/src/internal/observe/observe.go @@ -15,13 +15,14 @@ import ( "github.com/vbauerster/mpb/v8" "github.com/vbauerster/mpb/v8/decor" + "github.com/alcionai/corso/src/internal/common/color" "github.com/alcionai/corso/src/pkg/logger" ) const ( hideProgressBarsFN = "hide-progress" retainProgressBarsFN = "retain-progress" - progressBarWidth = 32 + progressBarWidth = 40 ) func init() { @@ -170,8 +171,15 @@ const ( // Progress Updates // --------------------------------------------------------------------------- +type ProgressCfg struct { + NewSection bool + SectionIdentifier any + Indent int + CompletionMessage func() string +} + // Message is used to display a progress message -func Message(ctx context.Context, msgs ...any) { +func Message(ctx context.Context, cfg ProgressCfg, msgs ...any) { var ( obs = getObserver(ctx) plainSl = make([]string, 0, len(msgs)) @@ -186,23 +194,42 @@ func Message(ctx context.Context, msgs ...any) { plain := strings.Join(plainSl, " ") loggable := strings.Join(loggableSl, " ") + if cfg.SectionIdentifier != nil { + plain = fmt.Sprintf( + "%s %s %s", + plain, + Bullet, + color.MagentaOutput(plainString(cfg.SectionIdentifier))) + loggable = fmt.Sprintf("%s - %v", loggable, cfg.SectionIdentifier) + } + logger.Ctx(ctx).Info(loggable) if obs.hidden() { return } + if cfg.NewSection { + // Empty bar to separate out section + obs.wg.Add(1) + empty := obs.mp.New(-1, mpb.NopStyle()) + empty.SetTotal(-1, true) + waitAndCloseBar(ctx, empty, obs.wg, func() {})() + } + obs.wg.Add(1) bar := obs.mp.New( -1, mpb.NopStyle(), - mpb.PrependDecorators(decor.Name( - plain, - decor.WC{ - W: len(plain) + 1, - C: decor.DidentRight, - }))) + mpb.PrependDecorators( + decor.Name("", decor.WC{W: cfg.Indent * 2}), + decor.Name( + plain, + decor.WC{ + W: len(plain) + 1, + C: decor.DidentRight, + }))) // Complete the bar immediately bar.SetTotal(-1, true) @@ -213,16 +240,38 @@ func Message(ctx context.Context, msgs ...any) { // that switches to "done" when the completion channel is signalled func MessageWithCompletion( ctx context.Context, - msg any, + cfg ProgressCfg, + msgs ...any, ) chan<- struct{} { var ( - obs = getObserver(ctx) - plain = plainString(msg) - loggable = fmt.Sprintf("%v", msg) - log = logger.Ctx(ctx) - ch = make(chan struct{}, 1) + obs = getObserver(ctx) + log = logger.Ctx(ctx) + plainSl = make([]string, 0, len(msgs)) + loggableSl = make([]string, 0, len(msgs)) + ch = make(chan struct{}, 1) ) + for _, m := range msgs { + plainSl = append(plainSl, plainString(m)) + loggableSl = append(loggableSl, fmt.Sprintf("%v", m)) + } + + plain := strings.Join(plainSl, " ") + loggable := strings.Join(loggableSl, " ") + + if cfg.SectionIdentifier != nil { + plain = fmt.Sprintf( + "%s %s %s", + plain, + Bullet, + color.MagentaOutput(plainString(cfg.SectionIdentifier))) + loggable = fmt.Sprintf("%s - %v", loggable, cfg.SectionIdentifier) + } + + if cfg.Indent > 0 { + plain = color.CyanOutput(plain) + } + log.Info(loggable) if obs.hidden() { @@ -230,17 +279,45 @@ func MessageWithCompletion( return ch } + if cfg.NewSection { + // Empty bar to separate out section + obs.wg.Add(1) + empty := obs.mp.New(-1, mpb.NopStyle()) + empty.SetTotal(-1, true) + waitAndCloseBar(ctx, empty, obs.wg, func() {})() + } + obs.wg.Add(1) frames := []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"} + // https://github.com/vbauerster/mpb/issues/71 + bfoc := mpb.BarFillerOnComplete(color.GreenOutput("done")) + if cfg.CompletionMessage != nil { + bfoc = mpb.BarFillerMiddleware(func(base mpb.BarFiller) mpb.BarFiller { + filler := func(w io.Writer, st decor.Statistics) error { + if st.Completed { + msg := fmt.Sprintf("%s %s", color.GreenOutput("done"), color.GreyOutput(cfg.CompletionMessage())) + _, err := io.WriteString(w, msg) + + return err + } + + return base.Fill(w, st) + } + + return mpb.BarFillerFunc(filler) + }) + } + bar := obs.mp.New( -1, mpb.SpinnerStyle(frames...).PositionLeft(), mpb.PrependDecorators( - decor.Name(plain+":"), + decor.Name("", decor.WC{W: cfg.Indent * 2}), + decor.Name(plain, decor.WC{W: progressBarWidth - cfg.Indent*2, C: decor.DidentRight}), decor.Elapsed(decor.ET_STYLE_GO, decor.WC{W: 8})), - mpb.BarFillerOnComplete("done")) + bfoc) go listen( ctx, @@ -520,7 +597,9 @@ func CollectionProgress( obs.wg.Add(1) barOpts := []mpb.BarOption{ - mpb.PrependDecorators(decor.Name(string(category))), + mpb.PrependDecorators( + decor.Name("", decor.WC{W: 2}), + decor.Name(color.CyanOutput(string(category)))), mpb.AppendDecorators( decor.CurrentNoUnit("%d - ", decor.WCSyncSpace), decor.Name(plain)), diff --git a/src/internal/observe/observe_test.go b/src/internal/observe/observe_test.go index 063e6d42b..533b0d760 100644 --- a/src/internal/observe/observe_test.go +++ b/src/internal/observe/observe_test.go @@ -127,6 +127,30 @@ func (suite *ObserveProgressUnitSuite) TestCollectionProgress_unblockOnChannelCl }() } +func (suite *ObserveProgressUnitSuite) TestObserve_section() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + recorder := strings.Builder{} + ctx = SeedObserver(ctx, &recorder, config{}) + + process := uuid.NewString()[:8] + target := uuid.NewString()[:8] + + pcfg := ProgressCfg{ + NewSection: true, + SectionIdentifier: target, + } + Message(ctx, pcfg, process) + + Flush(ctx) + assert.NotEmpty(t, recorder) + assert.Contains(t, recorder.String(), process) + assert.Contains(t, recorder.String(), target) +} + func (suite *ObserveProgressUnitSuite) TestObserve_message() { t := suite.T() @@ -138,7 +162,7 @@ func (suite *ObserveProgressUnitSuite) TestObserve_message() { message := uuid.NewString()[:8] - Message(ctx, message) + Message(ctx, ProgressCfg{}, message) Flush(ctx) assert.NotEmpty(t, recorder) assert.Contains(t, recorder.String(), message) @@ -155,7 +179,7 @@ func (suite *ObserveProgressUnitSuite) TestObserve_progressWithChannelClosed() { message := uuid.NewString()[:8] - ch := MessageWithCompletion(ctx, message) + ch := MessageWithCompletion(ctx, ProgressCfg{}, message) // Close channel without completing close(ch) @@ -180,7 +204,7 @@ func (suite *ObserveProgressUnitSuite) TestObserve_progressWithContextCancelled( message := uuid.NewString()[:8] - _ = MessageWithCompletion(ctx, message) + _ = MessageWithCompletion(ctx, ProgressCfg{}, message) // cancel context cancel() diff --git a/src/internal/operations/backup.go b/src/internal/operations/backup.go index 28a9a152f..0864d6088 100644 --- a/src/internal/operations/backup.go +++ b/src/internal/operations/backup.go @@ -279,7 +279,11 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) { // Execution // ----- - observe.Message(ctx, "Backing Up", observe.Bullet, clues.Hide(op.ResourceOwner.Name())) + pcfg := observe.ProgressCfg{ + NewSection: true, + SectionIdentifier: clues.Hide(op.ResourceOwner.Name()), + } + observe.Message(ctx, pcfg, "Backing Up") deets, err := op.do( ctx, @@ -528,7 +532,7 @@ func produceBackupDataCollections( counter *count.Bus, errs *fault.Bus, ) ([]data.BackupCollection, prefixmatcher.StringSetReader, bool, error) { - progressBar := observe.MessageWithCompletion(ctx, "Discovering items to backup") + progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Discovering items to backup") defer close(progressBar) bpc := inject.BackupProducerConfig{ @@ -565,7 +569,7 @@ func consumeBackupCollections( "collection_source", "operations", "snapshot_type", "item data") - progressBar := observe.MessageWithCompletion(ctx, "Backing up data") + progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Backing up data") defer close(progressBar) tags := map[string]string{ diff --git a/src/internal/operations/export.go b/src/internal/operations/export.go index efa7b84e5..685261c0d 100644 --- a/src/internal/operations/export.go +++ b/src/internal/operations/export.go @@ -223,7 +223,11 @@ func (op *ExportOperation) do( return nil, clues.Wrap(err, "getting backup and details") } - observe.Message(ctx, "Exporting", observe.Bullet, clues.Hide(bup.Selector.DiscreteOwner)) + pcfg := observe.ProgressCfg{ + NewSection: true, + SectionIdentifier: clues.Hide(bup.Selector.DiscreteOwner), + } + observe.Message(ctx, pcfg, "Exporting") paths, err := formatDetailsForRestoration(ctx, bup.Version, op.Selectors, deets, op.ec, op.Errors) if err != nil { @@ -239,9 +243,12 @@ func (op *ExportOperation) do( "backup_snapshot_id", bup.SnapshotID, "backup_version", bup.Version) - observe.Message(ctx, fmt.Sprintf("Discovered %d items in backup %s to export", len(paths), op.BackupID)) + observe.Message( + ctx, + observe.ProgressCfg{}, + fmt.Sprintf("Discovered %d items in backup %s to export", len(paths), op.BackupID)) - kopiaComplete := observe.MessageWithCompletion(ctx, "Enumerating items in repository") + kopiaComplete := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Enumerating items in repository") defer close(kopiaComplete) dcs, err := op.kopia.ProduceRestoreCollections(ctx, bup.SnapshotID, paths, opStats.bytesRead, op.Errors) @@ -332,7 +339,7 @@ func produceExportCollections( exportStats *data.ExportStats, errs *fault.Bus, ) ([]export.Collectioner, error) { - complete := observe.MessageWithCompletion(ctx, "Preparing export") + complete := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Preparing export") defer func() { complete <- struct{}{} close(complete) diff --git a/src/internal/operations/restore.go b/src/internal/operations/restore.go index de763351d..9f0eb3132 100644 --- a/src/internal/operations/restore.go +++ b/src/internal/operations/restore.go @@ -250,7 +250,11 @@ func (op *RestoreOperation) do( return nil, clues.WrapWC(ctx, graph.ErrServiceNotEnabled, "service not enabled for restore") } - observe.Message(ctx, "Restoring", observe.Bullet, clues.Hide(restoreToProtectedResource.Name())) + pcfg := observe.ProgressCfg{ + NewSection: true, + SectionIdentifier: clues.Hide(restoreToProtectedResource.Name()), + } + observe.Message(ctx, pcfg, "Restoring") paths, err := formatDetailsForRestoration( ctx, @@ -270,9 +274,16 @@ func (op *RestoreOperation) do( "backup_snapshot_id", bup.SnapshotID, "backup_version", bup.Version) - observe.Message(ctx, fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID)) + if len(paths) == 0 { + return nil, clues.New("no items match the provided filters") + } - progressBar := observe.MessageWithCompletion(ctx, "Enumerating items in repository") + observe.Message( + ctx, + observe.ProgressCfg{}, + fmt.Sprintf("Discovered %d items in backup %s to restore", len(paths), op.BackupID)) + + progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Enumerating items in repository") defer close(progressBar) dcs, err := op.kopia.ProduceRestoreCollections( @@ -380,7 +391,7 @@ func consumeRestoreCollections( errs *fault.Bus, ctr *count.Bus, ) (*details.Details, error) { - progressBar := observe.MessageWithCompletion(ctx, "Restoring data") + progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Restoring data") defer close(progressBar) rcc := inject.RestoreConsumerConfig{ diff --git a/src/pkg/repository/data_providers.go b/src/pkg/repository/data_providers.go index f8b919925..7db23f828 100644 --- a/src/pkg/repository/data_providers.go +++ b/src/pkg/repository/data_providers.go @@ -89,7 +89,7 @@ func connectToM365( return ctrl, nil } - progressBar := observe.MessageWithCompletion(ctx, "Connecting to M365") + progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Connecting to M365") defer close(progressBar) ctrl, err := m365.NewController( diff --git a/src/pkg/repository/repository.go b/src/pkg/repository/repository.go index 1bba0ac6c..185c8e695 100644 --- a/src/pkg/repository/repository.go +++ b/src/pkg/repository/repository.go @@ -153,7 +153,7 @@ func (r *repository) Initialize(ctx context.Context, cfg InitConfig) (err error) return clues.Stack(err) } - observe.Message(ctx, "Initializing repository") + observe.Message(ctx, observe.ProgressCfg{}, "Initializing repository") if err := r.setupKopia(ctx, cfg.RetentionOpts, true); err != nil { return err @@ -187,7 +187,8 @@ func (r *repository) Connect(ctx context.Context, cfg ConnConfig) (err error) { return clues.Stack(err) } - observe.Message(ctx, "Connecting to repository") + progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Connecting to repository") + defer close(progressBar) if err := r.setupKopia(ctx, ctrlRepo.Retention{}, false); err != nil { return clues.Stack(err) @@ -209,7 +210,7 @@ func (r *repository) UpdatePassword(ctx context.Context, password string) (err e } }() - progressBar := observe.MessageWithCompletion(ctx, "Connecting to repository") + progressBar := observe.MessageWithCompletion(ctx, observe.ProgressCfg{}, "Connecting to repository") defer close(progressBar) repoNameHash, err := r.GenerateHashForRepositoryConfigFileName()