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:
parent
f0cee163a8
commit
a2bde432e7
@ -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
|
||||||
}()
|
}()
|
||||||
|
|||||||
@ -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{}) {
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user