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.
This commit is contained in:
Keepers 2022-08-18 12:30:15 -06:00 committed by GitHub
parent c13ef798eb
commit 3ac05acf64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 49 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/alcionai/corso/cli/options" "github.com/alcionai/corso/cli/options"
. "github.com/alcionai/corso/cli/print" . "github.com/alcionai/corso/cli/print"
"github.com/alcionai/corso/cli/utils" "github.com/alcionai/corso/cli/utils"
"github.com/alcionai/corso/pkg/backup"
"github.com/alcionai/corso/pkg/logger" "github.com/alcionai/corso/pkg/logger"
"github.com/alcionai/corso/pkg/repository" "github.com/alcionai/corso/pkg/repository"
"github.com/alcionai/corso/pkg/selectors" "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")) return Only(ctx, errors.Wrap(err, "Unable to retrieve backup results from storage"))
} }
OutputBackup(ctx, *bu) bu.Print(ctx)
return nil return nil
} }
@ -290,12 +291,12 @@ func listExchangeCmd(cmd *cobra.Command, args []string) error {
} }
defer utils.CloseRepo(ctx, r) defer utils.CloseRepo(ctx, r)
rps, err := r.Backups(ctx) bs, err := r.Backups(ctx)
if err != nil { if err != nil {
return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository")) return Only(ctx, errors.Wrap(err, "Failed to list backups in the repository"))
} }
OutputBackups(ctx, rps) backup.PrintAll(ctx, bs)
return nil 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")) 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 return nil
} }

View File

@ -58,6 +58,7 @@ func BuildCommandTree(cmd *cobra.Command) {
cmd.PersistentPostRunE = config.InitFunc() cmd.PersistentPostRunE = config.InitFunc()
config.AddConfigFileFlag(cmd) config.AddConfigFileFlag(cmd)
print.AddOutputFlag(cmd) print.AddOutputFlag(cmd)
logger.AddLogLevelFlag(cmd)
cmd.CompletionOptions.DisableDefaultCmd = true cmd.CompletionOptions.DisableDefaultCmd = true

View File

@ -13,6 +13,7 @@ import (
. "github.com/alcionai/corso/cli/print" . "github.com/alcionai/corso/cli/print"
"github.com/alcionai/corso/pkg/account" "github.com/alcionai/corso/pkg/account"
"github.com/alcionai/corso/pkg/logger"
"github.com/alcionai/corso/pkg/storage" "github.com/alcionai/corso/pkg/storage"
) )
@ -133,7 +134,7 @@ func GetViper(ctx context.Context) *viper.Viper {
// set up properly. // set up properly.
func Read(ctx context.Context) error { func Read(ctx context.Context) error {
if err := viper.ReadInConfig(); err == nil { 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 err
} }
return nil return nil

View File

@ -9,9 +9,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tidwall/pretty" "github.com/tidwall/pretty"
"github.com/tomlazar/table" "github.com/tomlazar/table"
"github.com/alcionai/corso/pkg/backup"
"github.com/alcionai/corso/pkg/backup/details"
) )
var ( var (
@ -119,6 +116,13 @@ type Printable interface {
Values() []string 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 //revive:disable:redefines-builtin-id
func print(w io.Writer, p Printable) { func print(w io.Writer, p Printable) {
if outputAsJSON || outputAsJSONDebug { if outputAsJSON || outputAsJSONDebug {
@ -128,6 +132,12 @@ func print(w io.Writer, p Printable) {
outputTable(w, []Printable{p}) 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, // printAll prints the slice of printable items,
// according to the caller's requested format. // according to the caller's requested format.
func printAll(w io.Writer, ps []Printable) { func printAll(w io.Writer, ps []Printable) {
@ -141,33 +151,6 @@ func printAll(w io.Writer, ps []Printable) {
outputTable(w, ps) 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 // Tabular
// ------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------

View File

@ -1,9 +1,11 @@
package backup package backup
import ( import (
"context"
"fmt" "fmt"
"time" "time"
"github.com/alcionai/corso/cli/print"
"github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/internal/common"
"github.com/alcionai/corso/internal/connector/support" "github.com/alcionai/corso/internal/connector/support"
"github.com/alcionai/corso/internal/model" "github.com/alcionai/corso/internal/model"
@ -34,6 +36,9 @@ type Backup struct {
stats.StartAndEndTime stats.StartAndEndTime
} }
// interface compliance checks
var _ print.Printable = &Backup{}
func New( func New(
snapshotID, detailsID, status string, snapshotID, detailsID, status string,
selector selectors.Selector, selector selectors.Selector,
@ -55,6 +60,20 @@ func New(
// CLI Output // 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 { type Printable struct {
ID model.StableID `json:"id"` ID model.StableID `json:"id"`
ErrorCount int `json:"errorCount"` ErrorCount int `json:"errorCount"`
@ -66,7 +85,6 @@ type Printable struct {
// 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 {
// todo: implement printable backup struct
return Printable{ return Printable{
ID: b.ID, ID: b.ID,
ErrorCount: support.GetNumberOfErrors(b.ReadErrors) + support.GetNumberOfErrors(b.WriteErrors), ErrorCount: support.GetNumberOfErrors(b.ReadErrors) + support.GetNumberOfErrors(b.WriteErrors),

View File

@ -1,18 +1,48 @@
package details package details
import ( import (
"context"
"sync" "sync"
"time" "time"
"github.com/alcionai/corso/cli/print"
"github.com/alcionai/corso/internal/model" "github.com/alcionai/corso/internal/model"
) )
// --------------------------------------------------------------------------------
// Model
// --------------------------------------------------------------------------------
// DetailsModel describes what was stored in a Backup // DetailsModel describes what was stored in a Backup
type DetailsModel struct { type DetailsModel struct {
model.BaseModel model.BaseModel
Entries []DetailsEntry `json:"entries"` 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. // Details augments the core with a mutex for processing.
// Should be sliced back to d.DetailsModel for storage and // Should be sliced back to d.DetailsModel for storage and
// printing. // printing.
@ -29,6 +59,10 @@ func (d *Details) Add(repoRef string, info ItemInfo) {
d.Entries = append(d.Entries, DetailsEntry{RepoRef: repoRef, ItemInfo: info}) d.Entries = append(d.Entries, DetailsEntry{RepoRef: repoRef, ItemInfo: info})
} }
// --------------------------------------------------------------------------------
// Entry
// --------------------------------------------------------------------------------
// DetailsEntry describes a single item stored in a Backup // DetailsEntry describes a single item stored in a Backup
type DetailsEntry struct { type DetailsEntry struct {
// TODO: `RepoRef` is currently the full path to the item in Kopia // TODO: `RepoRef` is currently the full path to the item in Kopia
@ -37,15 +71,12 @@ type DetailsEntry struct {
ItemInfo ItemInfo
} }
// Paths returns the list of Paths extracted from the Entries slice. // --------------------------------------------------------------------------------
func (dm DetailsModel) Paths() []string { // CLI Output
ents := dm.Entries // --------------------------------------------------------------------------------
r := make([]string, len(ents))
for i := range ents { // interface compliance checks
r[i] = ents[i].RepoRef var _ print.Printable = &DetailsEntry{}
}
return r
}
// MinimumPrintable DetailsEntries is a passthrough func, because no // MinimumPrintable DetailsEntries is a passthrough func, because no
// reduction is needed for the json output. // reduction is needed for the json output.

View File

@ -4,13 +4,20 @@ import (
"context" "context"
"os" "os"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"github.com/alcionai/corso/cli/print"
) )
var ( var (
logCore *zapcore.Core logCore *zapcore.Core
loggerton *zap.SugaredLogger loggerton *zap.SugaredLogger
// logging level flag
// TODO: infer default based on environment.
llFlag = "info"
) )
type logLevel int type logLevel int
@ -22,6 +29,15 @@ const (
Production 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 { func singleton(level logLevel) *zap.SugaredLogger {
if loggerton != nil { if loggerton != nil {
return loggerton return loggerton
@ -79,16 +95,58 @@ type loggingKey string
const ctxKey loggingKey = "corsoLogger" const ctxKey loggingKey = "corsoLogger"
// Seed embeds a logger into the context for later retrieval. // Seed embeds a logger into the context for later retrieval.
func Seed(ctx context.Context) (context.Context, *zap.SugaredLogger) { // It also parses the command line for flag values prior to executing
l := singleton(0) // cobra. This early parsing is necessary since logging depends on
return context.WithValue(ctx, ctxKey, l), l // 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. // Ctx retrieves the logger embedded in the context.
func Ctx(ctx context.Context) *zap.SugaredLogger { func Ctx(ctx context.Context) *zap.SugaredLogger {
l := ctx.Value(ctxKey) l := ctx.Value(ctxKey)
if l == nil { if l == nil {
return singleton(0) return singleton(levelOf(llFlag))
} }
return l.(*zap.SugaredLogger) 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
}