From 3ac05acf644ecc73ac6da35dd96205c73646c542 Mon Sep 17 00:00:00 2001 From: Keepers Date: Thu, 18 Aug 2022 12:30:15 -0600 Subject: [PATCH] introduce log-level control (#589) Introduces the log-level flag, defaulting to info. Also does a minor refactor of how Print is called for backup results, which moves the backup/details imports out of the cli/print, and instead has thoses packages call a Print func. --- src/cli/backup/exchange.go | 9 +++-- src/cli/cli.go | 1 + src/cli/config/config.go | 3 +- src/cli/print/print.go | 43 ++++++-------------- src/pkg/backup/backup.go | 20 +++++++++- src/pkg/backup/details/details.go | 49 ++++++++++++++++++----- src/pkg/logger/logger.go | 66 +++++++++++++++++++++++++++++-- 7 files changed, 142 insertions(+), 49 deletions(-) diff --git a/src/cli/backup/exchange.go b/src/cli/backup/exchange.go index d6c1d0e64..92551511a 100644 --- a/src/cli/backup/exchange.go +++ b/src/cli/backup/exchange.go @@ -9,6 +9,7 @@ import ( "github.com/alcionai/corso/cli/options" . "github.com/alcionai/corso/cli/print" "github.com/alcionai/corso/cli/utils" + "github.com/alcionai/corso/pkg/backup" "github.com/alcionai/corso/pkg/logger" "github.com/alcionai/corso/pkg/repository" "github.com/alcionai/corso/pkg/selectors" @@ -208,7 +209,7 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error { return Only(ctx, errors.Wrap(err, "Unable to retrieve backup results from storage")) } - OutputBackup(ctx, *bu) + bu.Print(ctx) return nil } @@ -290,12 +291,12 @@ func listExchangeCmd(cmd *cobra.Command, args []string) error { } defer utils.CloseRepo(ctx, r) - rps, err := r.Backups(ctx) + bs, err := r.Backups(ctx) if err != nil { return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository")) } - OutputBackups(ctx, rps) + backup.PrintAll(ctx, bs) return nil } @@ -384,7 +385,7 @@ func detailsExchangeCmd(cmd *cobra.Command, args []string) error { return Only(ctx, errors.New("nothing to display: no items in the backup match the provided selectors")) } - OutputEntries(ctx, ds.Entries) + ds.PrintEntries(ctx) return nil } diff --git a/src/cli/cli.go b/src/cli/cli.go index ad2421734..1e8c5ff9c 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -58,6 +58,7 @@ func BuildCommandTree(cmd *cobra.Command) { cmd.PersistentPostRunE = config.InitFunc() config.AddConfigFileFlag(cmd) print.AddOutputFlag(cmd) + logger.AddLogLevelFlag(cmd) cmd.CompletionOptions.DisableDefaultCmd = true diff --git a/src/cli/config/config.go b/src/cli/config/config.go index 4112e1a18..8bcd971dc 100644 --- a/src/cli/config/config.go +++ b/src/cli/config/config.go @@ -13,6 +13,7 @@ import ( . "github.com/alcionai/corso/cli/print" "github.com/alcionai/corso/pkg/account" + "github.com/alcionai/corso/pkg/logger" "github.com/alcionai/corso/pkg/storage" ) @@ -133,7 +134,7 @@ func GetViper(ctx context.Context) *viper.Viper { // set up properly. func Read(ctx context.Context) error { if err := viper.ReadInConfig(); err == nil { - Info(ctx, "Using config file:", viper.ConfigFileUsed()) + logger.Ctx(ctx).Debugw("found config file", "configFile", viper.ConfigFileUsed()) return err } return nil diff --git a/src/cli/print/print.go b/src/cli/print/print.go index cc364134d..a6a64a9d1 100644 --- a/src/cli/print/print.go +++ b/src/cli/print/print.go @@ -9,9 +9,6 @@ import ( "github.com/spf13/cobra" "github.com/tidwall/pretty" "github.com/tomlazar/table" - - "github.com/alcionai/corso/pkg/backup" - "github.com/alcionai/corso/pkg/backup/details" ) var ( @@ -119,6 +116,13 @@ type Printable interface { Values() []string } +// Item prints the printable, according to the caller's requested format. +func Item(ctx context.Context, p Printable) { + print(getRootCmd(ctx).OutOrStdout(), p) +} + +// print prints the printable items, +// according to the caller's requested format. //revive:disable:redefines-builtin-id func print(w io.Writer, p Printable) { if outputAsJSON || outputAsJSONDebug { @@ -128,6 +132,12 @@ func print(w io.Writer, p Printable) { outputTable(w, []Printable{p}) } +// All prints the slice of printable items, +// according to the caller's requested format. +func All(ctx context.Context, ps ...Printable) { + printAll(getRootCmd(ctx).OutOrStdout(), ps) +} + // printAll prints the slice of printable items, // according to the caller's requested format. func printAll(w io.Writer, ps []Printable) { @@ -141,33 +151,6 @@ func printAll(w io.Writer, ps []Printable) { outputTable(w, ps) } -// ------------------------------------------------------------------------------------------ -// Type Formatters (TODO: migrate to owning packages) -// ------------------------------------------------------------------------------------------ - -// Prints the backup to the terminal with stdout. -func OutputBackup(ctx context.Context, b backup.Backup) { - print(getRootCmd(ctx).OutOrStdout(), b) -} - -// Prints the backups to the terminal with stdout. -func OutputBackups(ctx context.Context, bs []backup.Backup) { - ps := []Printable{} - for _, b := range bs { - ps = append(ps, b) - } - printAll(getRootCmd(ctx).OutOrStdout(), ps) -} - -// Prints the entries to the terminal with stdout. -func OutputEntries(ctx context.Context, des []details.DetailsEntry) { - ps := []Printable{} - for _, de := range des { - ps = append(ps, de) - } - printAll(getRootCmd(ctx).OutOrStdout(), ps) -} - // ------------------------------------------------------------------------------------------ // Tabular // ------------------------------------------------------------------------------------------ diff --git a/src/pkg/backup/backup.go b/src/pkg/backup/backup.go index 4df66b17d..1c6ae0710 100644 --- a/src/pkg/backup/backup.go +++ b/src/pkg/backup/backup.go @@ -1,9 +1,11 @@ package backup import ( + "context" "fmt" "time" + "github.com/alcionai/corso/cli/print" "github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/internal/connector/support" "github.com/alcionai/corso/internal/model" @@ -34,6 +36,9 @@ type Backup struct { stats.StartAndEndTime } +// interface compliance checks +var _ print.Printable = &Backup{} + func New( snapshotID, detailsID, status string, selector selectors.Selector, @@ -55,6 +60,20 @@ func New( // CLI Output // -------------------------------------------------------------------------------- +// Print writes the Backup to StdOut, in the format requested by the caller. +func (b Backup) Print(ctx context.Context) { + print.Item(ctx, b) +} + +// PrintAll writes the slice of Backups to StdOut, in the format requested by the caller. +func PrintAll(ctx context.Context, bs []Backup) { + ps := []print.Printable{} + for _, b := range bs { + ps = append(ps, print.Printable(b)) + } + print.All(ctx, ps...) +} + type Printable struct { ID model.StableID `json:"id"` ErrorCount int `json:"errorCount"` @@ -66,7 +85,6 @@ type Printable struct { // MinimumPrintable reduces the Backup to its minimally printable details. func (b Backup) MinimumPrintable() any { - // todo: implement printable backup struct return Printable{ ID: b.ID, ErrorCount: support.GetNumberOfErrors(b.ReadErrors) + support.GetNumberOfErrors(b.WriteErrors), diff --git a/src/pkg/backup/details/details.go b/src/pkg/backup/details/details.go index 7748b7cac..81e24c4ee 100644 --- a/src/pkg/backup/details/details.go +++ b/src/pkg/backup/details/details.go @@ -1,18 +1,48 @@ package details import ( + "context" "sync" "time" + "github.com/alcionai/corso/cli/print" "github.com/alcionai/corso/internal/model" ) +// -------------------------------------------------------------------------------- +// Model +// -------------------------------------------------------------------------------- + // DetailsModel describes what was stored in a Backup type DetailsModel struct { model.BaseModel Entries []DetailsEntry `json:"entries"` } +// Print writes the DetailModel Entries to StdOut, in the format +// requested by the caller. +func (dm DetailsModel) PrintEntries(ctx context.Context) { + ps := []print.Printable{} + for _, de := range dm.Entries { + ps = append(ps, de) + } + print.All(ctx, ps...) +} + +// Paths returns the list of Paths extracted from the Entries slice. +func (dm DetailsModel) Paths() []string { + ents := dm.Entries + r := make([]string, len(ents)) + for i := range ents { + r[i] = ents[i].RepoRef + } + return r +} + +// -------------------------------------------------------------------------------- +// Details +// -------------------------------------------------------------------------------- + // Details augments the core with a mutex for processing. // Should be sliced back to d.DetailsModel for storage and // printing. @@ -29,6 +59,10 @@ func (d *Details) Add(repoRef string, info ItemInfo) { d.Entries = append(d.Entries, DetailsEntry{RepoRef: repoRef, ItemInfo: info}) } +// -------------------------------------------------------------------------------- +// Entry +// -------------------------------------------------------------------------------- + // DetailsEntry describes a single item stored in a Backup type DetailsEntry struct { // TODO: `RepoRef` is currently the full path to the item in Kopia @@ -37,15 +71,12 @@ type DetailsEntry struct { ItemInfo } -// Paths returns the list of Paths extracted from the Entries slice. -func (dm DetailsModel) Paths() []string { - ents := dm.Entries - r := make([]string, len(ents)) - for i := range ents { - r[i] = ents[i].RepoRef - } - return r -} +// -------------------------------------------------------------------------------- +// CLI Output +// -------------------------------------------------------------------------------- + +// interface compliance checks +var _ print.Printable = &DetailsEntry{} // MinimumPrintable DetailsEntries is a passthrough func, because no // reduction is needed for the json output. diff --git a/src/pkg/logger/logger.go b/src/pkg/logger/logger.go index 99e2e97b2..fccae7960 100644 --- a/src/pkg/logger/logger.go +++ b/src/pkg/logger/logger.go @@ -4,13 +4,20 @@ import ( "context" "os" + "github.com/spf13/cobra" + "github.com/spf13/pflag" "go.uber.org/zap" "go.uber.org/zap/zapcore" + + "github.com/alcionai/corso/cli/print" ) var ( logCore *zapcore.Core loggerton *zap.SugaredLogger + // logging level flag + // TODO: infer default based on environment. + llFlag = "info" ) type logLevel int @@ -22,6 +29,15 @@ const ( Production ) +// adds the persistent flag --log-level to the provided command. +// defaults to "info". +// This is a hack for help displays. Due to seeding the context, we +// need to parse the log level before we execute the command. +func AddLogLevelFlag(parent *cobra.Command) { + fs := parent.PersistentFlags() + fs.StringVar(&llFlag, "log-level", "info", "set the log level to debug|info|warn|error") +} + func singleton(level logLevel) *zap.SugaredLogger { if loggerton != nil { return loggerton @@ -79,16 +95,58 @@ type loggingKey string const ctxKey loggingKey = "corsoLogger" // Seed embeds a logger into the context for later retrieval. -func Seed(ctx context.Context) (context.Context, *zap.SugaredLogger) { - l := singleton(0) - return context.WithValue(ctx, ctxKey, l), l +// It also parses the command line for flag values prior to executing +// cobra. This early parsing is necessary since logging depends on +// a seeded context prior to cobra evaluating flags. +func Seed(ctx context.Context) (ctxOut context.Context, zsl *zap.SugaredLogger) { + level := Info + + // this func handles composing the return values whether or not an error occurs + defer func() { + zsl = singleton(level) + ctxOut = context.WithValue(ctx, ctxKey, zsl) + }() + + fs := pflag.NewFlagSet("seed-logger", pflag.ContinueOnError) + fs.ParseErrorsWhitelist.UnknownFlags = true + fs.String("log-level", "info", "set the log level to debug|info|warn|error") + + // parse the os args list to find the log level flag + if err := fs.Parse(os.Args[1:]); err != nil { + print.Err(ctx, err.Error()) + return + } + + // retrieve the user's preferred log level + // automatically defaults to "info" + levelString, err := fs.GetString("log-level") + if err != nil { + print.Err(ctx, err.Error()) + return + } + + level = levelOf(levelString) + return // return values handled in defer } // Ctx retrieves the logger embedded in the context. func Ctx(ctx context.Context) *zap.SugaredLogger { l := ctx.Value(ctxKey) if l == nil { - return singleton(0) + return singleton(levelOf(llFlag)) } return l.(*zap.SugaredLogger) } + +// transforms the llevel flag value to a logLevel enum +func levelOf(lvl string) logLevel { + switch lvl { + case "debug": + return Development + case "warn": + return Warn + case "error": + return Production + } + return Info +}