add progress bar toggle to cli flags (#1264)

## Description

Adds a flag that allows users to toggle off the progress
bar spinners.  Primarily useful when running in a lower
log level such as debug.

## Type of change

- [x] 🌻 Feature

## Issue(s)

* #1112

## Test Plan

- [x] 💪 Manual
This commit is contained in:
Keepers 2022-10-24 10:15:59 -06:00 committed by GitHub
parent f0cee163a8
commit a2bde432e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 112 additions and 46 deletions

View File

@ -66,6 +66,7 @@ func BuildCommandTree(cmd *cobra.Command) {
cmd.PersistentPostRunE = config.InitFunc() cmd.PersistentPostRunE = config.InitFunc()
config.AddConfigFlags(cmd) config.AddConfigFlags(cmd)
logger.AddLogLevelFlag(cmd) logger.AddLogLevelFlag(cmd)
observe.AddProgressBarFlags(cmd)
print.AddOutputFlag(cmd) print.AddOutputFlag(cmd)
options.AddGlobalOperationFlags(cmd) options.AddGlobalOperationFlags(cmd)
@ -87,11 +88,11 @@ func BuildCommandTree(cmd *cobra.Command) {
func Handle() { func Handle() {
ctx := config.Seed(context.Background()) ctx := config.Seed(context.Background())
ctx = print.SetRootCmd(ctx, corsoCmd) ctx = print.SetRootCmd(ctx, corsoCmd)
observe.SeedWriter(ctx, print.StderrWriter(ctx)) observe.SeedWriter(ctx, print.StderrWriter(ctx), observe.PreloadFlags())
BuildCommandTree(corsoCmd) BuildCommandTree(corsoCmd)
ctx, log := logger.Seed(ctx) ctx, log := logger.Seed(ctx, logger.PreloadLogLevel())
defer func() { defer func() {
_ = log.Sync() // flush all logs in the buffer _ = log.Sync() // flush all logs in the buffer
}() }()

View File

@ -4,28 +4,79 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os"
"sync" "sync"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/vbauerster/mpb/v8" "github.com/vbauerster/mpb/v8"
"github.com/vbauerster/mpb/v8/decor" "github.com/vbauerster/mpb/v8/decor"
) )
const progressBarWidth = 32 const (
noProgressBarsFN = "no-progress-bars"
progressBarWidth = 32
)
var ( var (
wg sync.WaitGroup wg sync.WaitGroup
con context.Context con context.Context
writer io.Writer writer io.Writer
progress *mpb.Progress progress *mpb.Progress
cfg *config
) )
func init() { func init() {
cfg = &config{}
makeSpinFrames(progressBarWidth) makeSpinFrames(progressBarWidth)
} }
// adds the persistent boolean flag --no-progress-bars to the provided command.
// This is a hack for help displays. Due to seeding the context, we also
// need to parse the configuration before we execute the command.
func AddProgressBarFlags(parent *cobra.Command) {
fs := parent.PersistentFlags()
fs.Bool(noProgressBarsFN, false, "turn off the progress bar displays")
}
// Due to races between the lazy evaluation of flags in cobra and the need to init observer
// behavior in a ctx, these options get pre-processed manually here using pflags. The canonical
// AddProgressBarFlag() ensures the flags are displayed as part of the help/usage output.
func PreloadFlags() bool {
fs := pflag.NewFlagSet("seed-observer", pflag.ContinueOnError)
fs.ParseErrorsWhitelist.UnknownFlags = true
fs.Bool(noProgressBarsFN, false, "turn off the progress bar displays")
// prevents overriding the corso/cobra help processor
fs.BoolP("help", "h", false, "")
// parse the os args list to find the log level flag
if err := fs.Parse(os.Args[1:]); err != nil {
return false
}
// retrieve the user's preferred display
// automatically defaults to "info"
shouldHide, err := fs.GetBool(noProgressBarsFN)
if err != nil {
return false
}
return shouldHide
}
// ---------------------------------------------------------------------------
// configuration
// ---------------------------------------------------------------------------
// config handles observer configuration
type config struct {
doNotDisplay bool
}
// SeedWriter adds default writer to the observe package. // SeedWriter adds default writer to the observe package.
// Uses a noop writer until seeded. // Uses a noop writer until seeded.
func SeedWriter(ctx context.Context, w io.Writer) { func SeedWriter(ctx context.Context, w io.Writer, hide bool) {
writer = w writer = w
con = ctx con = ctx
@ -33,6 +84,10 @@ func SeedWriter(ctx context.Context, w io.Writer) {
con = context.Background() con = context.Background()
} }
cfg = &config{
doNotDisplay: hide,
}
progress = mpb.NewWithContext( progress = mpb.NewWithContext(
con, con,
mpb.WithWidth(progressBarWidth), mpb.WithWidth(progressBarWidth),
@ -48,14 +103,18 @@ func Complete() {
progress.Wait() progress.Wait()
} }
SeedWriter(con, writer) SeedWriter(con, writer, cfg.doNotDisplay)
} }
// ---------------------------------------------------------------------------
// Progress for Known Quantities
// ---------------------------------------------------------------------------
// ItemProgress tracks the display of an item by counting the bytes // ItemProgress tracks the display of an item by counting the bytes
// read through the provided readcloser, up until the byte count matches // read through the provided readcloser, up until the byte count matches
// the totalBytes. // the totalBytes.
func ItemProgress(rc io.ReadCloser, iname string, totalBytes int64) (io.ReadCloser, func()) { func ItemProgress(rc io.ReadCloser, iname string, totalBytes int64) (io.ReadCloser, func()) {
if writer == nil || rc == nil || totalBytes == 0 { if cfg.doNotDisplay || writer == nil || rc == nil || totalBytes == 0 {
return rc, func() {} return rc, func() {}
} }
@ -77,6 +136,10 @@ func ItemProgress(rc io.ReadCloser, iname string, totalBytes int64) (io.ReadClos
return bar.ProxyReader(rc), waitAndCloseBar(bar) return bar.ProxyReader(rc), waitAndCloseBar(bar)
} }
// ---------------------------------------------------------------------------
// Progress for Unknown Quantities
// ---------------------------------------------------------------------------
var spinFrames []string var spinFrames []string
// The bar width is set to a static 32 characters. The default spinner is only // The bar width is set to a static 32 characters. The default spinner is only
@ -109,7 +172,7 @@ func makeSpinFrames(barWidth int) {
// incrementing the count of items handled. Each write to the provided channel // incrementing the count of items handled. Each write to the provided channel
// counts as a single increment. The caller is expected to close the channel. // counts as a single increment. The caller is expected to close the channel.
func CollectionProgress(user, category, dirName string) (chan<- struct{}, func()) { func CollectionProgress(user, category, dirName string) (chan<- struct{}, func()) {
if writer == nil || len(user) == 0 || len(dirName) == 0 { if cfg.doNotDisplay || writer == nil || len(user) == 0 || len(dirName) == 0 {
ch := make(chan struct{}) ch := make(chan struct{})
go func(ci <-chan struct{}) { go func(ci <-chan struct{}) {

View File

@ -32,13 +32,13 @@ func (suite *ObserveProgressUnitSuite) TestItemProgress() {
t := suite.T() t := suite.T()
recorder := strings.Builder{} recorder := strings.Builder{}
observe.SeedWriter(ctx, &recorder) observe.SeedWriter(ctx, &recorder, false)
defer func() { defer func() {
// don't cross-contaminate other tests. // don't cross-contaminate other tests.
observe.Complete() observe.Complete()
//nolint:forbidigo //nolint:forbidigo
observe.SeedWriter(context.Background(), nil) observe.SeedWriter(context.Background(), nil, false)
}() }()
from := make([]byte, 100) from := make([]byte, 100)
@ -85,13 +85,13 @@ func (suite *ObserveProgressUnitSuite) TestCollectionProgress_unblockOnCtxCancel
t := suite.T() t := suite.T()
recorder := strings.Builder{} recorder := strings.Builder{}
observe.SeedWriter(ctx, &recorder) observe.SeedWriter(ctx, &recorder, false)
defer func() { defer func() {
// don't cross-contaminate other tests. // don't cross-contaminate other tests.
observe.Complete() observe.Complete()
//nolint:forbidigo //nolint:forbidigo
observe.SeedWriter(context.Background(), nil) observe.SeedWriter(context.Background(), nil, false)
}() }()
progCh, closer := observe.CollectionProgress("test", "testcat", "testertons") progCh, closer := observe.CollectionProgress("test", "testcat", "testertons")
@ -120,13 +120,13 @@ func (suite *ObserveProgressUnitSuite) TestCollectionProgress_unblockOnChannelCl
t := suite.T() t := suite.T()
recorder := strings.Builder{} recorder := strings.Builder{}
observe.SeedWriter(ctx, &recorder) observe.SeedWriter(ctx, &recorder, false)
defer func() { defer func() {
// don't cross-contaminate other tests. // don't cross-contaminate other tests.
observe.Complete() observe.Complete()
//nolint:forbidigo //nolint:forbidigo
observe.SeedWriter(context.Background(), nil) observe.SeedWriter(context.Background(), nil, false)
}() }()
progCh, closer := observe.CollectionProgress("test", "testcat", "testertons") progCh, closer := observe.CollectionProgress("test", "testcat", "testertons")

View File

@ -8,8 +8,6 @@ import (
"github.com/spf13/pflag" "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/src/cli/print"
) )
var ( var (
@ -29,13 +27,40 @@ const (
Production Production
) )
const logLevelFN = "log-level"
// adds the persistent flag --log-level to the provided command. // adds the persistent flag --log-level to the provided command.
// defaults to "info". // defaults to "info".
// This is a hack for help displays. Due to seeding the context, we // This is a hack for help displays. Due to seeding the context, we also
// need to parse the log level before we execute the command. // need to parse the log level before we execute the command.
func AddLogLevelFlag(parent *cobra.Command) { func AddLogLevelFlag(parent *cobra.Command) {
fs := parent.PersistentFlags() fs := parent.PersistentFlags()
fs.StringVar(&llFlag, "log-level", "info", "set the log level to debug|info|warn|error") fs.StringVar(&llFlag, logLevelFN, "info", "set the log level to debug|info|warn|error")
}
// Due to races between the lazy evaluation of flags in cobra and the need to init logging
// behavior in a ctx, log-level gets pre-processed manually here using pflags. The canonical
// AddLogLevelFlag() ensures the flag is displayed as part of the help/usage output.
func PreloadLogLevel() string {
fs := pflag.NewFlagSet("seed-logger", pflag.ContinueOnError)
fs.ParseErrorsWhitelist.UnknownFlags = true
fs.String(logLevelFN, "info", "set the log level to debug|info|warn|error")
// prevents overriding the corso/cobra help processor
fs.BoolP("help", "h", false, "")
// parse the os args list to find the log level flag
if err := fs.Parse(os.Args[1:]); err != nil {
return "info"
}
// retrieve the user's preferred log level
// automatically defaults to "info"
levelString, err := fs.GetString(logLevelFN)
if err != nil {
return "info"
}
return levelString
} }
func genLogger(level logLevel) (*zapcore.Core, *zap.SugaredLogger) { func genLogger(level logLevel) (*zapcore.Core, *zap.SugaredLogger) {
@ -119,38 +144,15 @@ const ctxKey loggingKey = "corsoLogger"
// It also parses the command line for flag values prior to executing // It also parses the command line for flag values prior to executing
// cobra. This early parsing is necessary since logging depends on // cobra. This early parsing is necessary since logging depends on
// a seeded context prior to cobra evaluating flags. // a seeded context prior to cobra evaluating flags.
func Seed(ctx context.Context) (ctxOut context.Context, zsl *zap.SugaredLogger) { func Seed(ctx context.Context, lvl string) (context.Context, *zap.SugaredLogger) {
level := Info if len(lvl) == 0 {
lvl = "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")
// prevents overriding the corso/cobra help processor
fs.BoolP("help", "h", false, "")
// 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 zsl := singleton(levelOf(lvl))
// automatically defaults to "info" ctxOut := context.WithValue(ctx, ctxKey, zsl)
levelString, err := fs.GetString("log-level")
if err != nil {
print.Err(ctx, err.Error())
return
}
level = levelOf(levelString) return ctxOut, zsl
return // return values handled in defer
} }
// SeedLevel embeds a logger into the context with the given log-level. // SeedLevel embeds a logger into the context with the given log-level.